Conversation
The browse server was dying between Bash tool invocations in Claude Code because: 1. SIGTERM: The Claude Code sandbox sends SIGTERM to all child processes when a Bash command completes. The server received this and called shutdown(), deleting the state file and exiting. 2. Parent watchdog: The server polls BROWSE_PARENT_PID every 15s. When the parent Bash shell exits (killed by sandbox), the watchdog detected it and called shutdown(). Both mechanisms made it impossible to use the browse tool across multiple Bash calls — every new `$B` invocation started a fresh server with no cookies, no page state, and no tabs. Fix: - SIGTERM handler: log and ignore instead of shutdown. Explicit shutdown is still available via the /stop command or SIGINT (Ctrl+C). - Parent watchdog: log once and continue instead of shutdown. The existing idle timeout (30 min) handles eventual cleanup. The /stop command and SIGINT still work for intentional shutdown. Windows behavior is unchanged (uses taskkill /F which bypasses signal handlers). Tested: browse server survives across 5+ separate Bash tool calls in Claude Code, maintaining cookies, page state, and navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 16, 2026
PR #994 made browse persist across Claude Code Bash calls by ignoring SIGTERM and parent-PID death, relying on the 30-min idle timeout for eventual cleanup. Codex outside-voice review caught that the idle timeout doesn't apply in two modes: headed mode (/open-gstack-browser) and tunnel mode (/pair-agent). Both early-return from idleCheckInterval. Combined with #994's ignore-SIGTERM, those sessions would leak forever after the user disconnects — a real resource leak on shared machines where multiple /pair-agent sessions come and go. Fix: gate SIGTERM-ignore and parent-PID-watchdog-ignore to normal (headless) mode only. Headed + tunnel modes respect both signals and shutdown cleanly. Idle timeout behavior unchanged. Also documents the deliberate contract change for future contributors — don't re-add global SIGTERM shutdown thinking it's missing; it's intentionally scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 16, 2026
VERSION bump + users-first CHANGELOG entry for the wave: - #993 tilde-in-assignment fix (byliu-labs) - #994 browse server persists across Bash calls (joelgreen) - #996 cookie picker alive after cli exits (voidborne-d) - #864 OpenClaw skills codex-friendly (cathrynlavery) - #982 OpenCode native setup (breakneo) - #892 Windows cookie import + DPAPI + v20 CDP fallback (msr-hickory) Plus 3 follow-up hardening commits we own: - Extended tilde fix to design resolver + 4 more skill templates - Gated #994 SIGTERM-ignore to normal mode only (headed/tunnel preserve shutdown) - Documented CDP debug port security + log Chrome version on v20 fallback Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 of 5 tasks
Owner
|
Thanks @joelgreen! Landing this in wave PR #1028 (v0.18.1.0). Closing here. 🚀 |
garrytan
added a commit
that referenced
this pull request
Apr 16, 2026
PR #994 made browse persist across Claude Code Bash calls by ignoring SIGTERM and parent-PID death, relying on the 30-min idle timeout for eventual cleanup. Codex outside-voice review caught that the idle timeout doesn't apply in two modes: headed mode (/open-gstack-browser) and tunnel mode (/pair-agent). Both early-return from idleCheckInterval. Combined with #994's ignore-SIGTERM, those sessions would leak forever after the user disconnects — a real resource leak on shared machines where multiple /pair-agent sessions come and go. Fix: gate SIGTERM-ignore and parent-PID-watchdog-ignore to normal (headless) mode only. Headed + tunnel modes respect both signals and shutdown cleanly. Idle timeout behavior unchanged. Also documents the deliberate contract change for future contributors — don't re-add global SIGTERM shutdown thinking it's missing; it's intentionally scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 16, 2026
VERSION bump + users-first CHANGELOG entry for the wave: - #993 tilde-in-assignment fix (byliu-labs) - #994 browse server persists across Bash calls (joelgreen) - #996 cookie picker alive after cli exits (voidborne-d) - #864 OpenClaw skills codex-friendly (cathrynlavery) - #982 OpenCode native setup (breakneo) - #892 Windows cookie import + DPAPI + v20 CDP fallback (msr-hickory) Plus 3 follow-up hardening commits we own: - Extended tilde fix to design resolver + 4 more skill templates - Gated #994 SIGTERM-ignore to normal mode only (headed/tunnel preserve shutdown) - Documented CDP debug port security + log Chrome version on v20 fallback Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 17, 2026
Resolves overlap between main #1025 (headed browser auto-shutdown fix) and this wave's #994 (server persists across Bash calls) + our SIGTERM mode-gate follow-up. Both touch the parent-PID watchdog and SIGTERM handlers. CHANGELOG / VERSION: - main shipped v0.18.1.0 with #1025 while this wave was open. Bumped this branch to v0.18.2.0 with its own entry on top, per CLAUDE.md branch-scoped CHANGELOG rules. Both entries preserved. - package.json synced to v0.18.2.0. browse/src/server.ts (parent-PID watchdog): - Combined main's outer env-var gate (`BROWSE_HEADED=1` skips the watchdog entirely) with our inner mode check (if watchdog runs and parent dies, decision tree by mode: active picker > headed/tunnel > normal). - Defense-in-depth: env var skips the setInterval cost, runtime mode check catches anything the env var missed. browse/src/server.ts (SIGTERM handler): - Combined main's lambda wrap (avoids signal name being passed as exitCode → NaN → exit 1) with our mode-aware handler (active picker > headed/tunnel shutdown > normal ignore). - Also wrapped SIGINT in a lambda — main's #1025 fix applied to that handler too, but our wave didn't have that fix. Now SIGINT exits with code 0 reliably. browse/test/watchdog.test.ts: - Inverted test 3's expectation: pre-#994 the server died on parent death; post-#994 the server STAYS ALIVE (the whole point — multi-step QA across Claude Code Bash calls now works). Test now verifies isProcessAlive(serverPid) is true after a 20-second observation window with the parent killed. - Updated docstring to document all 4 watchdog invariants (env-var disable, headed-mode disable, headless persists, tunnel shuts down). - Tunnel mode coverage left as documentation rather than test — tunnelActive is a runtime variable set by /pair-agent, not an env var, so faking it would require invasive test-only hooks. Verification: full bun test passes (EXIT=0). Watchdog test: 3 pass, 0 fail (22.7s including the 20s observation window). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 17, 2026
Resolves overlap between main #1025 (headed browser auto-shutdown fix) and this wave's #994 (server persists across Bash calls) + our SIGTERM mode-gate follow-up. Both touch the parent-PID watchdog and SIGTERM handlers. Mechanical resolution only — test inversion follows in next commit. CHANGELOG / VERSION: - main shipped v0.18.1.0 with #1025 while this wave was open. Bumped this branch to v0.18.2.0 with its own entry on top, per CLAUDE.md branch-scoped CHANGELOG rules. Both entries preserved, contiguous version sequence. - package.json synced to v0.18.2.0. browse/src/server.ts (parent-PID watchdog): - Combined main's outer env-var gate (BROWSE_HEADED=1 skips the watchdog entirely) with our inner mode check (when watchdog runs and parent dies, decision tree by mode: active picker > headed/tunnel > normal). - Defense-in-depth: env var skips the setInterval cost, runtime mode check catches anything the env var missed. browse/src/server.ts (SIGTERM/SIGINT handlers): - Combined main's lambda wrap (avoids signal name being passed as exitCode → NaN → exit 1) with our mode-aware SIGTERM handler. Active picker check short-circuits regardless of mode. - Wrapped SIGINT in a lambda too — main's #1025 fix applied to that handler, but our wave didn't have it. Now SIGINT exits with code 0 reliably. Note: browse/test/watchdog.test.ts test 3 (added by main) currently FAILS because main's "watchdog kills on parent death" expectation contradicts post-#994 "server stays alive on parent death." Inverted in the next commit.
garrytan
added a commit
that referenced
this pull request
Apr 17, 2026
main #1025 added browse/test/watchdog.test.ts with test 3 expecting the old "watchdog kills server when parent dies" behavior. The merge with this branch's #994 inverted that semantic — the server now STAYS ALIVE on parent death in normal headless mode (multi-step QA across Claude Code Bash calls depends on this). Changes: - Renamed test 3 from "watchdog fires when parent dies" to "server STAYS ALIVE when parent dies (#994)". - Replaced 25s shutdown poll with 20s observation window asserting the server remains alive after the watchdog tick. - Updated docstring to document all 3 watchdog invariants (env-var disable, headed-mode disable, headless persists) and note tunnel-mode coverage gap. Verification: bun test browse/test/watchdog.test.ts → 3 pass, 0 fail (22.7s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 17, 2026
* fix: extend tilde-in-assignment fix to design resolver + 4 skill templates PR #993 fixed the Claude Code permission prompt for `scripts/resolvers/browse.ts` and `gstack-upgrade/SKILL.md.tmpl`. Same bug lives in three more places that weren't on the contributor's branch: - `scripts/resolvers/design.ts` (3 spots: D=, B=, and _DESIGN_DIR=) - `design-shotgun/SKILL.md.tmpl` (_DESIGN_DIR=) - `plan-design-review/SKILL.md.tmpl` (_DESIGN_DIR=) - `design-consultation/SKILL.md.tmpl` (_DESIGN_DIR=) - `design-review/SKILL.md.tmpl` (REPORT_DIR=) Replaces bare `~/` with quoted `"$HOME/..."` in the source-of-truth files, then regenerates. `grep -rEn '^[A-Za-z_]+=~/' --include="SKILL.md" .` now returns zero hits across all hosts (claude, codex, cursor, gbrain, hermes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(openclaw): make native skills codex-friendly (#864) Normalizes YAML frontmatter on the 4 hand-authored OpenClaw skills so stricter parsers like Codex can load them. Codex CLI was rejecting these files with "mapping values are not allowed in this context" on colons inside unquoted description scalars. - Drops non-standard `version` and `metadata` fields - Rewrites descriptions into simple "Use when..." form (no inline colons) - Adds a regression test enforcing strict frontmatter (name + description only) Verified live: Codex CLI now loads the skills without errors. Observed during /codex outside-voice run on the eval-community-prs plan review — Codex stderr tripped on these exact files, which was real-world confirmation the fix is needed. Dropped the connect-chrome changes from the original PR (the symlink removal is out of scope for this fix; keeping connect-chrome -> open-gstack-browser). Co-Authored-By: Cathryn Lavery <cathrynlavery@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): server persists across Claude Code Bash calls The browse server was dying between Bash tool invocations in Claude Code because: 1. SIGTERM: The Claude Code sandbox sends SIGTERM to all child processes when a Bash command completes. The server received this and called shutdown(), deleting the state file and exiting. 2. Parent watchdog: The server polls BROWSE_PARENT_PID every 15s. When the parent Bash shell exits (killed by sandbox), the watchdog detected it and called shutdown(). Both mechanisms made it impossible to use the browse tool across multiple Bash calls — every new `$B` invocation started a fresh server with no cookies, no page state, and no tabs. Fix: - SIGTERM handler: log and ignore instead of shutdown. Explicit shutdown is still available via the /stop command or SIGINT (Ctrl+C). - Parent watchdog: log once and continue instead of shutdown. The existing idle timeout (30 min) handles eventual cleanup. The /stop command and SIGINT still work for intentional shutdown. Windows behavior is unchanged (uses taskkill /F which bypasses signal handlers). Tested: browse server survives across 5+ separate Bash tool calls in Claude Code, maintaining cookies, page state, and navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): gate #994 SIGTERM-ignore to normal mode only PR #994 made browse persist across Claude Code Bash calls by ignoring SIGTERM and parent-PID death, relying on the 30-min idle timeout for eventual cleanup. Codex outside-voice review caught that the idle timeout doesn't apply in two modes: headed mode (/open-gstack-browser) and tunnel mode (/pair-agent). Both early-return from idleCheckInterval. Combined with #994's ignore-SIGTERM, those sessions would leak forever after the user disconnects — a real resource leak on shared machines where multiple /pair-agent sessions come and go. Fix: gate SIGTERM-ignore and parent-PID-watchdog-ignore to normal (headless) mode only. Headed + tunnel modes respect both signals and shutdown cleanly. Idle timeout behavior unchanged. Also documents the deliberate contract change for future contributors — don't re-add global SIGTERM shutdown thinking it's missing; it's intentionally scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: keep cookie picker alive after cli exits Fixes #985 * fix: add opencode setup support * feat(browse): add Windows browser path detection and DPAPI cookie decryption - Extend BrowserPlatform to include win32 - Add windowsDataDir to BrowserInfo; populate for Chrome, Edge, Brave, Chromium - getBaseDir('win32') → ~/AppData/Local - findBrowserMatch checks Network/Cookies first on Windows (Chrome 80+) - Add getWindowsAesKey() reading os_crypt.encrypted_key from Local State JSON - Add dpapiDecrypt() via PowerShell ProtectedData.Unprotect (stdin/stdout) - decryptCookieValue branches on platform: AES-256-GCM (Windows) vs AES-128-CBC (mac/linux) - Fix hardcoded /tmp → TEMP_DIR from platform.ts in openDbFromCopy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(browse): Windows cookie import — profile discovery, v20 detection, CDP fallback Three bugs fixed in cookie-import-browser.ts: - listProfiles() and findInstalledBrowsers() now check Network/Cookies on Windows (Chrome 80+ moved cookies from profile/Cookies to profile/Network/Cookies) - openDb() always uses copy-then-read on Windows (Chrome holds exclusive locks) - decryptCookieValue() detects v20 App-Bound Encryption with specific error code Added CDP-based extraction fallback (importCookiesViaCdp) for v20 cookies: - Launches Chrome headless with --remote-debugging-port on the real profile - Extracts cookies via Network.getAllCookies over CDP WebSocket - Requires Chrome to be closed (v20 keys are path-bound to user-data-dir) - Both cookie picker UI and CLI direct-import paths auto-fall back to CDP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): document CDP debug port security + log Chrome version on v20 fallback Follow-up to #892 per Codex outside-voice review. Two small additions to the Windows v20 App-Bound Encryption CDP fallback: 1. Inline comment documenting the deliberate security posture of the --remote-debugging-port. Chrome binds it to 127.0.0.1 by default, so the threat model is local-user-only (which is no worse than baseline — local attackers can already read the cookie DB). Random port 9222-9321 is for collision avoidance, not security. Chrome is always killed in finally. 2. One-time Chrome version log on CDP entry via /json/version. When Chrome inevitably changes v20 key format or /json/list shape in a future major version, logs will show exactly which version users are hitting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: v0.18.1.0 — community wave (6 PRs + hardening) VERSION bump + users-first CHANGELOG entry for the wave: - #993 tilde-in-assignment fix (byliu-labs) - #994 browse server persists across Bash calls (joelgreen) - #996 cookie picker alive after cli exits (voidborne-d) - #864 OpenClaw skills codex-friendly (cathrynlavery) - #982 OpenCode native setup (breakneo) - #892 Windows cookie import + DPAPI + v20 CDP fallback (msr-hickory) Plus 3 follow-up hardening commits we own: - Extended tilde fix to design resolver + 4 more skill templates - Gated #994 SIGTERM-ignore to normal mode only (headed/tunnel preserve shutdown) - Documented CDP debug port security + log Chrome version on v20 fallback Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: review pass — package.json version, import dedup, error context, stale help Findings from /review on the wave PR: - [P1] package.json version was 0.18.0.1 but VERSION is 0.18.1.0, failing test/gen-skill-docs.test.ts:177 "package.json version matches VERSION file". Bumped package.json to 0.18.1.0. - [P2] Duplicate import of cookie-picker-routes in browse/src/server.ts (handleCookiePickerRoute at line 20 + hasActivePicker at line 792). Merged into single import at top. - [P2] cookie-import-browser.ts:494 generic rethrow loses underlying error. Now preserves the message so "ENOENT" vs "JSON parse error" vs "permission denied" are distinguishable in user output. - [P3] setup:46 "Missing value for --host" error message listed an incomplete set of hosts (missing factory, openclaw, hermes, gbrain). Aligned with the "Unknown value" error on line 94. Kept as-is (not real issues): - cookie-import-browser.ts:869 empty catch on Chrome version fetch is the correct pattern for best-effort diagnostics (per slop-scan philosophy in CLAUDE.md — fire-and-forget failures shouldn't throw). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(watchdog): invert test 3 to match merged #994 behavior main #1025 added browse/test/watchdog.test.ts with test 3 expecting the old "watchdog kills server when parent dies" behavior. The merge with this branch's #994 inverted that semantic — the server now STAYS ALIVE on parent death in normal headless mode (multi-step QA across Claude Code Bash calls depends on this). Changes: - Renamed test 3 from "watchdog fires when parent dies" to "server STAYS ALIVE when parent dies (#994)". - Replaced 25s shutdown poll with 20s observation window asserting the server remains alive after the watchdog tick. - Updated docstring to document all 3 watchdog invariants (env-var disable, headed-mode disable, headless persists) and note tunnel-mode coverage gap. Verification: bun test browse/test/watchdog.test.ts → 3 pass, 0 fail (22.7s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): switch apt mirror to Hetzner to bypass Ubicloud → archive.ubuntu.com timeouts Both build attempts of `.github/docker/Dockerfile.ci` failed at `apt-get update` with persistent connection timeouts to archive.ubuntu.com:80 and security.ubuntu.com:80 — 90+ seconds of "connection timed out" against every Ubuntu IP. Not a transient blip; this PR doesn't touch the Dockerfile, and a re-run reproduced the same failure across all 9 mirror IPs. Root cause: Ubicloud runners (Hetzner FSN1-DC21 per runner output) have unreliable HTTP-port-80 routing to Ubuntu's official archive endpoints. Fix: - Rewrite /etc/apt/sources.list.d/ubuntu.sources (deb822 format in 24.04) to use https://mirror.hetzner.com/ubuntu/packages instead. Hetzner's mirror is publicly accessible from any cloud (not Hetzner-only despite the name) and route-local for Ubicloud's actual host. Solves both reliability and latency. - Add a 3-attempt retry loop around both `apt-get update` calls as belt-and-suspenders. Even Hetzner's mirror can have brief blips, and the retry costs nothing when the first attempt succeeds. Verification: the workflow will rebuild on push. Local `docker build` not practical for a 12-step image with bun + claude + playwright deps + a 10-min cold install. Trusting CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): use HTTP for Hetzner apt mirror (base image lacks ca-certificates) Previous commit switched to https://mirror.hetzner.com/... which proved the mirror is reachable and routes correctly (no more 90s timeouts), but exposed a chicken-and-egg: ubuntu:24.04 ships without ca-certificates, and that's exactly the package we're installing. Result: "No system certificates available. Try installing ca-certificates." Fix: use http:// for the Hetzner mirror. Apt's security model verifies package integrity via GPG-signed Release files, not TLS, so HTTP here is no weaker than the upstream defaults (Ubuntu's official sources also default to HTTP for the same reason). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cathryn Lavery <cathrynlavery@users.noreply.github.com> Co-authored-by: Joel Green <thejoelgreen@gmail.com> Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com> Co-authored-by: Break <breakneo@gmail.com> Co-authored-by: Michael Spitzer-Rubenstein <msr.ext@hickory.ai>
JerkyJesse
added a commit
to JerkyJesse/GrakStack
that referenced
this pull request
Apr 18, 2026
Ports gstack garrytan#994 (joelgreen) + garrytan#1020 gating + SIGINT wrapper fix (garrytan#1025). The browse server was dying between Claude Code Bash tool invocations because: 1. SIGTERM: Claude Code sandbox sends SIGTERM to child processes when a Bash command completes. Server called shutdown() and deleted state file. 2. Parent watchdog: 15s BROWSE_PARENT_PID poll shut down on parent death. Both made every new $B call start a fresh server with no cookies or state. Fix (scoped to normal headless mode): - SIGTERM handler: in headless mode, log and ignore. Idle timeout (30min) handles eventual cleanup. Headed/tunnel modes respect SIGTERM and shutdown cleanly (idle timeout early-returns in those modes, so ignoring would leak). - Parent watchdog: same three-way resolution. Active cookie picker always stays alive to avoid stranding picker UI with "Failed to fetch". - SIGINT (Ctrl+C) always shuts down — intentional stop. - Outer IS_HEADED_WATCHDOG gate skips watchdog entirely in headed mode. - shutdown() called via arrow wrapper so signal name arg doesn't become NaN exitCode (fixes legacy exit-code-1 bug). Imports hasActivePicker() from cookie-picker-routes (added in prior commit). /open-cavestack-browser path used in place of /open-gstack-browser in inline docs. Upstream: garrytan/gstack@1211b6b Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
JerkyJesse
added a commit
to JerkyJesse/GrakStack
that referenced
this pull request
Apr 18, 2026
Ports 5 upstream PRs from garrytan#1028 (community wave v0.18.1.0): - garrytan#892 Windows cookie import (DPAPI + v20 CDP fallback) - garrytan#996 Cookie picker survives CLI exit - garrytan#994 + garrytan#1020 SIGTERM persistence + headless-mode gating - garrytan#993 Tilde-in-assignment fix (design resolver + 5 templates) - garrytan#864 OpenClaw native skill frontmatter normalization (codex-friendly) Plus local change: caveman mode locked to full (lite/ultra removed). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 of 5 tasks
mathiasmora2232
pushed a commit
to mathiasmora2232/gstack
that referenced
this pull request
May 30, 2026
* fix: extend tilde-in-assignment fix to design resolver + 4 skill templates PR garrytan#993 fixed the Claude Code permission prompt for `scripts/resolvers/browse.ts` and `gstack-upgrade/SKILL.md.tmpl`. Same bug lives in three more places that weren't on the contributor's branch: - `scripts/resolvers/design.ts` (3 spots: D=, B=, and _DESIGN_DIR=) - `design-shotgun/SKILL.md.tmpl` (_DESIGN_DIR=) - `plan-design-review/SKILL.md.tmpl` (_DESIGN_DIR=) - `design-consultation/SKILL.md.tmpl` (_DESIGN_DIR=) - `design-review/SKILL.md.tmpl` (REPORT_DIR=) Replaces bare `~/` with quoted `"$HOME/..."` in the source-of-truth files, then regenerates. `grep -rEn '^[A-Za-z_]+=~/' --include="SKILL.md" .` now returns zero hits across all hosts (claude, codex, cursor, gbrain, hermes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(openclaw): make native skills codex-friendly (garrytan#864) Normalizes YAML frontmatter on the 4 hand-authored OpenClaw skills so stricter parsers like Codex can load them. Codex CLI was rejecting these files with "mapping values are not allowed in this context" on colons inside unquoted description scalars. - Drops non-standard `version` and `metadata` fields - Rewrites descriptions into simple "Use when..." form (no inline colons) - Adds a regression test enforcing strict frontmatter (name + description only) Verified live: Codex CLI now loads the skills without errors. Observed during /codex outside-voice run on the eval-community-prs plan review — Codex stderr tripped on these exact files, which was real-world confirmation the fix is needed. Dropped the connect-chrome changes from the original PR (the symlink removal is out of scope for this fix; keeping connect-chrome -> open-gstack-browser). Co-Authored-By: Cathryn Lavery <cathrynlavery@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): server persists across Claude Code Bash calls The browse server was dying between Bash tool invocations in Claude Code because: 1. SIGTERM: The Claude Code sandbox sends SIGTERM to all child processes when a Bash command completes. The server received this and called shutdown(), deleting the state file and exiting. 2. Parent watchdog: The server polls BROWSE_PARENT_PID every 15s. When the parent Bash shell exits (killed by sandbox), the watchdog detected it and called shutdown(). Both mechanisms made it impossible to use the browse tool across multiple Bash calls — every new `$B` invocation started a fresh server with no cookies, no page state, and no tabs. Fix: - SIGTERM handler: log and ignore instead of shutdown. Explicit shutdown is still available via the /stop command or SIGINT (Ctrl+C). - Parent watchdog: log once and continue instead of shutdown. The existing idle timeout (30 min) handles eventual cleanup. The /stop command and SIGINT still work for intentional shutdown. Windows behavior is unchanged (uses taskkill /F which bypasses signal handlers). Tested: browse server survives across 5+ separate Bash tool calls in Claude Code, maintaining cookies, page state, and navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): gate garrytan#994 SIGTERM-ignore to normal mode only PR garrytan#994 made browse persist across Claude Code Bash calls by ignoring SIGTERM and parent-PID death, relying on the 30-min idle timeout for eventual cleanup. Codex outside-voice review caught that the idle timeout doesn't apply in two modes: headed mode (/open-gstack-browser) and tunnel mode (/pair-agent). Both early-return from idleCheckInterval. Combined with garrytan#994's ignore-SIGTERM, those sessions would leak forever after the user disconnects — a real resource leak on shared machines where multiple /pair-agent sessions come and go. Fix: gate SIGTERM-ignore and parent-PID-watchdog-ignore to normal (headless) mode only. Headed + tunnel modes respect both signals and shutdown cleanly. Idle timeout behavior unchanged. Also documents the deliberate contract change for future contributors — don't re-add global SIGTERM shutdown thinking it's missing; it's intentionally scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: keep cookie picker alive after cli exits Fixes garrytan#985 * fix: add opencode setup support * feat(browse): add Windows browser path detection and DPAPI cookie decryption - Extend BrowserPlatform to include win32 - Add windowsDataDir to BrowserInfo; populate for Chrome, Edge, Brave, Chromium - getBaseDir('win32') → ~/AppData/Local - findBrowserMatch checks Network/Cookies first on Windows (Chrome 80+) - Add getWindowsAesKey() reading os_crypt.encrypted_key from Local State JSON - Add dpapiDecrypt() via PowerShell ProtectedData.Unprotect (stdin/stdout) - decryptCookieValue branches on platform: AES-256-GCM (Windows) vs AES-128-CBC (mac/linux) - Fix hardcoded /tmp → TEMP_DIR from platform.ts in openDbFromCopy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(browse): Windows cookie import — profile discovery, v20 detection, CDP fallback Three bugs fixed in cookie-import-browser.ts: - listProfiles() and findInstalledBrowsers() now check Network/Cookies on Windows (Chrome 80+ moved cookies from profile/Cookies to profile/Network/Cookies) - openDb() always uses copy-then-read on Windows (Chrome holds exclusive locks) - decryptCookieValue() detects v20 App-Bound Encryption with specific error code Added CDP-based extraction fallback (importCookiesViaCdp) for v20 cookies: - Launches Chrome headless with --remote-debugging-port on the real profile - Extracts cookies via Network.getAllCookies over CDP WebSocket - Requires Chrome to be closed (v20 keys are path-bound to user-data-dir) - Both cookie picker UI and CLI direct-import paths auto-fall back to CDP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): document CDP debug port security + log Chrome version on v20 fallback Follow-up to garrytan#892 per Codex outside-voice review. Two small additions to the Windows v20 App-Bound Encryption CDP fallback: 1. Inline comment documenting the deliberate security posture of the --remote-debugging-port. Chrome binds it to 127.0.0.1 by default, so the threat model is local-user-only (which is no worse than baseline — local attackers can already read the cookie DB). Random port 9222-9321 is for collision avoidance, not security. Chrome is always killed in finally. 2. One-time Chrome version log on CDP entry via /json/version. When Chrome inevitably changes v20 key format or /json/list shape in a future major version, logs will show exactly which version users are hitting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: v0.18.1.0 — community wave (6 PRs + hardening) VERSION bump + users-first CHANGELOG entry for the wave: - garrytan#993 tilde-in-assignment fix (byliu-labs) - garrytan#994 browse server persists across Bash calls (joelgreen) - garrytan#996 cookie picker alive after cli exits (voidborne-d) - garrytan#864 OpenClaw skills codex-friendly (cathrynlavery) - garrytan#982 OpenCode native setup (breakneo) - garrytan#892 Windows cookie import + DPAPI + v20 CDP fallback (msr-hickory) Plus 3 follow-up hardening commits we own: - Extended tilde fix to design resolver + 4 more skill templates - Gated garrytan#994 SIGTERM-ignore to normal mode only (headed/tunnel preserve shutdown) - Documented CDP debug port security + log Chrome version on v20 fallback Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: review pass — package.json version, import dedup, error context, stale help Findings from /review on the wave PR: - [P1] package.json version was 0.18.0.1 but VERSION is 0.18.1.0, failing test/gen-skill-docs.test.ts:177 "package.json version matches VERSION file". Bumped package.json to 0.18.1.0. - [P2] Duplicate import of cookie-picker-routes in browse/src/server.ts (handleCookiePickerRoute at line 20 + hasActivePicker at line 792). Merged into single import at top. - [P2] cookie-import-browser.ts:494 generic rethrow loses underlying error. Now preserves the message so "ENOENT" vs "JSON parse error" vs "permission denied" are distinguishable in user output. - [P3] setup:46 "Missing value for --host" error message listed an incomplete set of hosts (missing factory, openclaw, hermes, gbrain). Aligned with the "Unknown value" error on line 94. Kept as-is (not real issues): - cookie-import-browser.ts:869 empty catch on Chrome version fetch is the correct pattern for best-effort diagnostics (per slop-scan philosophy in CLAUDE.md — fire-and-forget failures shouldn't throw). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(watchdog): invert test 3 to match merged garrytan#994 behavior main garrytan#1025 added browse/test/watchdog.test.ts with test 3 expecting the old "watchdog kills server when parent dies" behavior. The merge with this branch's garrytan#994 inverted that semantic — the server now STAYS ALIVE on parent death in normal headless mode (multi-step QA across Claude Code Bash calls depends on this). Changes: - Renamed test 3 from "watchdog fires when parent dies" to "server STAYS ALIVE when parent dies (garrytan#994)". - Replaced 25s shutdown poll with 20s observation window asserting the server remains alive after the watchdog tick. - Updated docstring to document all 3 watchdog invariants (env-var disable, headed-mode disable, headless persists) and note tunnel-mode coverage gap. Verification: bun test browse/test/watchdog.test.ts → 3 pass, 0 fail (22.7s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): switch apt mirror to Hetzner to bypass Ubicloud → archive.ubuntu.com timeouts Both build attempts of `.github/docker/Dockerfile.ci` failed at `apt-get update` with persistent connection timeouts to archive.ubuntu.com:80 and security.ubuntu.com:80 — 90+ seconds of "connection timed out" against every Ubuntu IP. Not a transient blip; this PR doesn't touch the Dockerfile, and a re-run reproduced the same failure across all 9 mirror IPs. Root cause: Ubicloud runners (Hetzner FSN1-DC21 per runner output) have unreliable HTTP-port-80 routing to Ubuntu's official archive endpoints. Fix: - Rewrite /etc/apt/sources.list.d/ubuntu.sources (deb822 format in 24.04) to use https://mirror.hetzner.com/ubuntu/packages instead. Hetzner's mirror is publicly accessible from any cloud (not Hetzner-only despite the name) and route-local for Ubicloud's actual host. Solves both reliability and latency. - Add a 3-attempt retry loop around both `apt-get update` calls as belt-and-suspenders. Even Hetzner's mirror can have brief blips, and the retry costs nothing when the first attempt succeeds. Verification: the workflow will rebuild on push. Local `docker build` not practical for a 12-step image with bun + claude + playwright deps + a 10-min cold install. Trusting CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): use HTTP for Hetzner apt mirror (base image lacks ca-certificates) Previous commit switched to https://mirror.hetzner.com/... which proved the mirror is reachable and routes correctly (no more 90s timeouts), but exposed a chicken-and-egg: ubuntu:24.04 ships without ca-certificates, and that's exactly the package we're installing. Result: "No system certificates available. Try installing ca-certificates." Fix: use http:// for the Hetzner mirror. Apt's security model verifies package integrity via GPG-signed Release files, not TLS, so HTTP here is no weaker than the upstream defaults (Ubuntu's official sources also default to HTTP for the same reason). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cathryn Lavery <cathrynlavery@users.noreply.github.com> Co-authored-by: Joel Green <thejoelgreen@gmail.com> Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com> Co-authored-by: Break <breakneo@gmail.com> Co-authored-by: Michael Spitzer-Rubenstein <msr.ext@hickory.ai>
garrytan
added a commit
that referenced
this pull request
Aug 16, 2026
…-billed evals, required Linux lane (#2593) * ci: bump CI image Bun 1.3.10 -> 1.3.13 Matches the local toolchain and brings native `bun test --shard=M/N` / --parallel to CI (needed by the free-test lane and shard runner work). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: stop version bumps rebuilding the eval Docker image (cache key trio) Three coupled fixes, atomic because any subset is worse than none: 1. Image tag keys on hashFiles(Dockerfile.ci, bun.lock) — package.json is out: its version field changed on 60/60 recent commits, forcing a ~2min image rebuild per PR for a dependency set only bun.lock determines. 2. ci-image.yml now pushes that same content-hash tag (previously only :latest/:sha, so the weekly prebuild never warmed the tag the eval matrix actually looks up) and both eval workflows get registry layer cache (cache-to export gated to same-repo runs; fork tokens cannot write GHCR). 3. Dockerfile bakes /opt/node_modules_cache/.bun.lock and the runtime Restore-deps guard diffs bun.lock instead of package.json — otherwise every version-only bump made all 14 matrix jobs fall back to a live bun install, which is slower than today's behavior. Worst-case failure mode is self-healing: a missing tag or cache falls back to exactly the previous rebuild-and-install path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: stop double-running lint + skill-docs on every PR commit Both fired on unrestricted push AND pull_request, so each PR push ran them twice (12 duplicate (headSha, workflow) pairs in the last 200 runs). push is now main-only; pull_request covers PR branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run actionlint from the prebuilt image (16s -> ~2s) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: right-size five single-core jobs to ubicloud-standard-2 actionlint, skill-docs, version-gate, pr-title-sync, and the evals report job never exceed one core; standard-8 was ~4x the cost for zero wall-clock. build-image and the eval matrix keep standard-8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: fix workflow_dispatch concurrency collisions (head_ref || run_id) head_ref is empty on workflow_dispatch, so every manual dispatch of these four workflows shared one empty-suffix group and cancelled each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(windows): cache bun installs; run the curated suite, not a hand list - actions/cache on ~/.bun/install/cache keyed on bun.lock (install was 35-45s of both 55-64s jobs, all network) and Bun pinned to 1.3.13 to match the other lanes. - windows-free-tests now runs `bun run test:windows` (the runner's --windows-only curation) instead of a hand-listed 13-file subset that had drifted from the registry it sampled. POSIX-bound tests get excluded in ONE place (the curation patterns), not two. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: retry 1, not 2, on every paid path Measured on the llm-judge shard: --retry 2 amplified 25 tests into 46 executions (+84%), with retried runs at 138s vs a 10-12s baseline (429 backoff), and a permanently-failing test paying 3x. One retry still absorbs one-off flakes; chronic flakes become visible fix-work instead of silent wall-clock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: split skill-e2e-review into three per-file CI shards Bun runs describe blocks as concurrency barriers, so the e2e-review CI job executed its tests serially: 741s of an 860s PR critical path for tests whose slowest member is 224s. The per-file matrix is the repo's parallelism unit, so the split moves: - Retro E2E + retro-base-branch -> test/skill-e2e-retro.test.ts - review/ship base-branch + Review Dashboard Via Attribution -> test/skill-e2e-review-attribution.test.ts - sql-injection / enum-completeness / design-lite stay in test/skill-e2e-review.test.ts One 741s job becomes three ~180-250s jobs. Locally the worst paid shard drops from 1705s (94.7% of the 1800s kill) to under 700s. Test names, bodies, suite strings, and eval-store collectors are unchanged, so baselines carry over. Matrix rows added to both eval workflows (attribution is gate-only, so no periodic row); the report job's hardcoded runner count is gone (drift-proof). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: gate security-bench on SECURITY_BENCH=1, not model-cache existence The existsSync gate ran ~12s of ONNX inference (plus a HuggingFace dataset fetch) on every free-suite run on any dev box that had ever warmed the classifier, while CI (no cache) silently skipped it. Now explicit opt-in: SECURITY_BENCH=1 bun test browse/test/security-bench.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: watchdog E2E in 1.5s instead of 22.7s (tunable poll interval) server.ts gains BROWSE_WATCHDOG_INTERVAL_MS (floor 50ms, default 15s unchanged). The #994 stay-alive test runs a 250ms tick and waits for the stay-alive log line instead of blind-sleeping 2s + 20s past the production interval. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: dedupe coverage gates; route both walks through skill-census skill-coverage-floor duplicated two matrix assertions (registry completeness, gate-tier floor) with a DIFFERENT hand-rolled directory walk — matrix's skipped nothing, floor's skipped node_modules/docs/test. Two 'same' gates disagreeing on the census is the bug class test/helpers/skill-census.ts was written to kill. Registry assertions now live in matrix only (with floor's better error message), both files walk via skillCensus().authoredSkills, and floor keeps the per-skill structural checks it owns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: EVALS_JOBS for shard processes; explicit within-shard concurrency EVALS_CONCURRENCY was overloaded: the legacy bun-test path used it as --max-concurrency (default 15) while the sharded runner read it as the process count — exporting the legacy value gave 15 concurrent Bun processes each spawning claude (the 429 storm). Now: EVALS_JOBS = shard processes (default 4); EVALS_CONCURRENCY = bun --max-concurrency inside a shard (default 4, explicit in shard args — omitting it made within-shard parallelism silently differ from the legacy path). Stale 49/59 header math replaced with the live-count rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: enforce detach-timeout floor from the live shard census New free tripwire: eval:bg:gate / eval:bg:periodic --timeout must cover ceil(shards/jobs) x shard-timeout x 1.05, recomputed from the actual paid test census every run. Hand-derived numbers go stale every time a paid file lands — the review split just proved it: periodic's 28800s dropped BELOW its new 32130s worst case (raised to 32400s here). An undersized watchdog kills healthy runs and the tail reports never-started. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: preflight ping once in the sharded parent, not per shard The Anthropic fail-fast ping ran at module load in every paid test file importing e2e-helpers — ~30 paid claude -p calls (30s timeout each) per full sharded run for one bit of information. The parent now pings once before spawning shards and sets EVALS_PREFLIGHT_OK=1; the module-load path honors the flag. Extracted to test/helpers/anthropic-preflight.ts (injectable spawn seam) with regression pins in both directions: the flag must skip, its absence must ping exactly once, dead API must throw. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: split touchfiles into pure data + selection logic + facade touchfiles.ts listed ITSELF in GLOBAL_TOUCHFILES, so adding one test's dep entry forced the full ~$38 / 30-45min suite — measured on 21.9% of recent commits (42/192). The self-reference existed because data and logic shared a file: any edit COULD be a selection-logic change. Now: touchfiles-data.ts (the four maps, literals only, zero imports — the future map-diff target), test-selection.ts (matchGlob/detectBase Branch/getChangedFiles/selectTests), and touchfiles.ts as a re-export facade so all ~12 import sites are untouched. GLOBAL_TOUCHFILES drops the self-ref, adds test-selection.ts (logic stays maximally conservative), and TEMPORARILY adds touchfiles-data.ts until the map-diff change lands. New free test pins the literal-only property (comment-aware state-machine scan with a self-test) and facade export parity (===), so neither can silently rot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: free runner — strict output, parallel execution, stable shard indices Three coupled changes to scripts/test-free-shards.ts: 1. STRICT OUTPUT: runFreeShard streams through the paid runner's BunTestOutputClassifier — exit 0 without bun's 'Ran N tests across M files' summary, with (fail) lines, or with a wrong file count is a FAILURE (anti-truncation backstop at the runner layer), plus an external wall-clock timeout that SIGKILLs the process group (timed-out distinct from failed; exit 124 vs 1). Also fixes a latent shard-bleed: file selectors now use exactTestFileSelectors (relative paths were substring filters that matched sibling roots). 2. PARALLEL: full-suite mode is one 'bun test --parallel' invocation (Bun 1.3.13). Measured semantics recorded in the header: per-file worker isolation, standard summary, and mid-suite process.exit surfaces as a crashed-worker FAIL with exit 1 — strictly safer than serial, where the same exit truncates silently. No static weight lists; --shards M --shard i keeps deterministic hash partitioning for CI matrices (native --shard rejected: round-robin renumbers when files land). Spawned shards get throwaway GSTACK_HOME/TMPDIR so parallel shards can't contend on real state. Per-shard epilogue prints files/seconds/status every run. 3. Stable indices: assignFilesToShards no longer drops empty shards, so a shard's index depends only on the file hash and requested count — an empty CI matrix slot is a fast no-op success, not a renumbering. package.json 'test' now delegates to the runner (TEST_ROOTS becomes the single source of truth for roots; slop:diff tail preserved; the runner inherits the 30s per-test timeout the old glob passed inline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: Linux free-test lane — ~400 files get CI coverage for the first time New required, secretless free-tests job: the canonical runner's single 'bun test --parallel' invocation with strict-output classification on ubicloud-standard-8. The free suite previously ran on NO Linux CI — only a curated Windows subset ran anywhere — so every 'tests pass' claim about main rested on contributors running them locally. Secretless by design (no API keys; fork PRs finally get real test signal) and pinned by test/free-tests-workflow-wiring.test.ts: canonical runner invoked, zero secrets.* references, pull_request never pull_request_target, and matrix-count/--shards agreement if anyone switches to the sharded fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: map-diff selection — a touchfiles-data edit runs only what changed Editing the eval dep-list data no longer forces the full ~$38 / 30-45min suite (measured on 21.9% of recent commits). When touchfiles-data.ts is in the diff, selection now evaluates the BASE version (git show -> mkdtemp -> spawnSync bun child printing the four maps as JSON — sync because e2e-helpers selects at module scope) and JSON-diffs per key: added entries, edited dep lists, and tier flips are selected; keys removed from all maps are reported, never silently dropped; a GLOBAL_TOUCHFILES edit still runs everything. FAIL-CLOSED with named causes: missing-base-ref, git-show-failed, import-failed, shape-mismatch each degrade to run-all and print 'selection: global — touchfiles-data changed (<cause>)' (D9 — silently expensive beats silently wrong, but never silently). eval:select prints 'selected N of M, reason: ...' + removed tests; --base scopes the map-diff too. The temporary conservative GLOBAL entry for touchfiles-data.ts is gone — its changes route through the map-diff. 23 new free tests: pure-core fixtures, selectTests wiring incl. a poison-injection guard, and a temp git repo exercising every fail-closed cause end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: selection sees uncommitted work; git errors fail closed getChangedFiles is now the deduped union of committed (base...HEAD), staged+unstaged (git diff HEAD), and untracked (git status --porcelain --untracked-files=all) — an agent that edits files and runs evals BEFORE committing no longer gets the full $38 suite every time because the committed diff looked empty. Clean tree still returns [] (run-all by design for main-branch/periodic runs). Git failures now THROW with the failing command, stderr, and 'set EVALS_ALL=1 to deliberately run the full suite' — the old return [] silently became run-all, which is silently expensive. 11 new free tests cover every source, dedupe, quoted paths, and both failure shapes via an injectable spawn seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: revert GSTACK_HOME injection in the free runner — shared mutable state The first full run under the strict runner surfaced 12 failures with one root cause: injecting a single throwaway GSTACK_HOME per invocation made 6,900 tests share a MUTABLE scratch home. gstack-config tests wrote keys into it; relink and update-check tests then read them (e.g. relink saw skill_prefix left behind by a config test and produced prefixed names). All 12 pass when run directly. TMPDIR isolation stays (mkdtemp inside it is still per-call unique). Tests needing GSTACK_HOME isolation mkdtemp their own per test — the repo convention — and hermetic-env covers E2E children. The env-dump pin now asserts GSTACK_HOME passes through UNTOUCHED so the injection can't come back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: rebase parity baseline to v1.64.0.0; fix capture-vs-check drift The parity ratchet had quietly failed for 7 skills — v1.58-v1.64 growth landed past the v1.57.7.0 anchors and nothing caught it because this test had no CI lane (verified pre-existing: SKILL.md content is byte-identical to origin/main). Same rebase protocol as v1.53->v1.57.7.0; old baseline retained for the audit trail. Root-caused a second latent bug while rebasing: captureBaseline recorded SKELETON-ONLY bytes while the checker compares UNION bytes (skeleton + carved sections/*.md), so a fresh capture read carved skills at ~2x ratio (ship: 82KB captured vs 183KB checked). captureBaseline now takes sectionedSkills and records unions for carved skills — capture and check measure the same thing, so the NEXT rebase can't hit this. Four CARVE_GUARDS skeleton caps re-ratcheted to current +headroom (plan-ceo 92K, plan-eng 70K, office-hours 100K, design-consultation 70K), annotated inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: package.json version matches VERSION (1.64.0.0) v1.64.0.0 shipped with VERSION bumped but package.json left at 1.63.0.0 — the 'package.json version matches VERSION file' test fails on origin/main today. Nothing caught it because that test had no CI lane until this branch's free-tests job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: fix variants-retry-after HTTP-date flake (TODOS P2) toUTCString() truncates to whole seconds, so a +3000ms Retry-After date could mean an effective wait of ~2001ms — flaking against the 2500ms assertion floor ~1-2 in 9 runs under suite load. +4000ms puts the truncation floor at 3001ms with the assertion floor safely below it. Pulled forward from U4 because the free-tests lane is now a required check and this flake would randomly block PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: skill-fixture helper — extract SKILL.md sections, don't copy files extractSkillSections (fence-aware H2 scanner, loud-throw on missing sections with available-heading list), extractSkillBody (drops the shared generated preamble), extractSkillHead (frontmatter + first 30 lines, for routing fixtures). Pinned section lists per consumer, and free-tier real-skill pins so a gen-skill-docs heading rename fails the FREE suite instead of a paid run. skill-fixture.ts joins GLOBAL_TOUCHFILES (fail-safe polarity: over-select). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): review E2E fixtures extract sections — 1871 -> 207 lines CLAUDE.md's extract-don't-copy rule, applied: the three review fixtures carry only the sections the sql-injection/enum/design-lite prompts and judges exercise (89% cut). Full-file copies made claude -p read 1871 lines per test — the direct cause of the 1705s worst shard (94.7% of the 1800s kill). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): retro E2E fixtures extract sections — 1821 -> 757 lines Keeps every section the retro flow exercises incl. base-branch detect; drops preamble, Global Retrospective Mode, Compare Mode (58% cut). retro-base-branch was the single slowest CI test at 224s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): review-army fixture extracts sections — 1871 -> 650 lines CS1's set plus Step 1.5 (PLAN COMPLETION AUDIT machinery) and Step 4.5 (army dispatch, quality_score, findings schema) that the 7 army tests assert on. Pin test guards the three load-bearing strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): skillify fixtures via extractSkillBody — 63-83% smaller Tests follow all 11 skillify steps, so the whole body stays; only the shared generated preamble drops (skillify 1239->453, scrape 958->167). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): context-skills fixtures via extractSkillBody — 74-82% smaller context-save 1037->267 lines, context-restore 952->168; the 8 tests exercise full save/restore/list flows so the body stays, preamble drops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): opus-47 discovery fixtures via extractSkillHead — ~95% smaller Routing/fanout tests only read frontmatter + opening lines of the 14 installed skills (review 1871->54, office-hours 1706->80). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): codex runner gains sections option — review variant 88% smaller runCodexSkill/installSkillToTempHome accept sections?: string[] routed through extractSkillSections; codex-review-findings wired (1465->181 lines). codex-discover-skill deliberately keeps the FULL copy — its stderr assertions validate that the real generated artifact loads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): routing fixture installs skill HEADS, not ~18 full SKILL.md Routing reads frontmatter only; extractSkillHead per skill (root 611->48, ship 1435->54 lines). This was the single worst fixture bloat site: one fixture dir holding ~18 full skills. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: parent-side shard skipping — a one-test diff runs 3 of 44 shards The sharded runner spawned every shard regardless of diff; only the child self-skipped, so a typical single-skill change still paid 44 Bun boots + container-equivalent setup for shards with zero selected tests. The parent now computes selection once (mirroring e2e-helpers exactly: EVALS_ALL -> run-all, empty union -> run-all, git errors propagate the fail-closed throw) and drops shards where no selected test name maps in. Mapping = quoted E2E map keys in the file's source UNION keys whose dep list registers the file (constructed-name families need the second direction). FAIL-OPEN everywhere it matters: run-all, non-skill-e2e files, unreadable source, zero mapped names all keep the shard — the child filter stays authoritative, so a parent bug can only run extra. New taxonomy status skipped-by-diff (never conflated with never-started); selection banner prints once; --list is selection-aware. C6 lands in the same commit: a HARD tier-alignment test — every paid skill-e2e file must be parent-mappable or provably fail-open-safe. Note: this change-set's 14 dep-list registrations in touchfiles-data.ts rode along in f945c841 (concurrent-agent staging); they belong to this change logically. Demo: selection of one test -> 'running 3 of 44 shards, 41 skipped-by-diff'. 13 new $0 tests via injected seams. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: fix context-save-list test that was 0-for-26 ($5.28, zero passes) Disposition for the eval store's only permanently-red test. Root cause: the hide-other-branches assertions scanned the FULL output surface (incl. bash tool_results), so any agent that ran ls on the checkpoints dir — the natural first step of a list flow — surfaced all three seeded filenames and failed, even when its user-facing listing filtered correctly. The test punished the agent for looking at the directory. The hide-assertions now scan the agent's FINAL TEXT (the listing the user sees); showsMain keeps the broad surface for its documented reason. Validated live in the final gate run rather than quarantined: the test guards real behavior (branch filtering) and the assertion was the bug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: wire 12 orphaned test files into the free suite (D3a) ios-qa/daemon/test (10 files), ios-qa/scripts/gen-accessors.test.ts, and browser-skills/hackernews-frontpage/script.test.ts ran under NO script or CI — written coverage catching nothing. All 174 tests green on arrival (4.6s), zero quarantines needed. TODOS P2 closed: main wired design/test in v1.64, the variants-retry-after flake it named is fixed on this branch, and this commit lands the remaining orphans. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: supabase-provision runs in-process — 16.5s -> 0.45s bin/gstack-gbrain-supabase-provision (482-line bash) becomes a 26-line bun-shebang entry over a new importable lib/gbrain-supabase-provision.ts with an injected-deps seam (fetch/env/stdout/sleep — D7: args, never env-mutation-before-import). The 33 spawn-per-test cases run in-process against the same Bun.serve mocks; exactly one spawn smoke keeps the shebang/CLI/receipt contract covered. Byte-compat proven by a 25-case differential harness (old bash bin from git vs new, same mock): stdout, stderr, exit codes identical across all subcommands, JSON/plain modes, and error paths. Egress receipts stay per-attempt, receipt-before-send, fail-closed (scanner updated: SHELL_SINKS -> MODULE_SINKS). No-op sleep injection makes retry/backoff paths instant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: kill the 3,372-line zombie monolith; revive 4 never-run tests test/skill-e2e.test.ts survived the v1.56 split as a zombie: the paid glob needs the skill-e2e-* hyphen, so with EVALS=1 NOTHING has executed it for ~8 releases — and it held the ONLY implementations of four map-registered tests: review-coverage-audit (gate), plan-eng-coverage- audit (gate), ship-triage (gate), ship-idempotency (periodic). Three gate tests silently never ran — the exact 0%-execution class this branch exists to kill. Rehomed into test/skill-e2e-coverage-audit.test.ts, -triage.test.ts, and -ship-idempotency-sdk.test.ts with bodies byte-identical modulo collector wiring and fixture extraction (drift observed in the skills since v1.56 is DOCUMENTED in each header, not fixed — their first paid run in 8 releases must attribute failures to drift, not to this move). All 24 other monolith names were true duplicates of the split files — dropped with the monolith. Matrix rows added to both eval workflows; the paid glob's zombie-exclusion is now a commented regression pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: fix two parallelism-exposed flakes (probe re-run, live-tree census) Both pass solo and on main but flaked under the parallel runner: 1. gstack-brain-context-load probed 'gbrain --version' PER QUERY with a 500ms budget — a cold probe on a saturated box timed out (observed 505ms), branding gbrain 'missing' for one query while siblings passed. The probe is now memoized (availability can't change mid-invocation) with a generous one-time 5s budget; query calls keep the tight timeout. 2. skill-size-budget's catalog estimate read the LIVE tree, so a concurrent worker's transient skill-shaped scratch dirs exactly doubled it (8356 vs 4177). The ratchet now counts git-TRACKED skills only — the catalog that ships, immune to sibling workers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: demote 4 expensive posture tests to periodic (D2a) design-consultation-research ($0.91/304s) and -preview ($0.89/481s) — the two most expensive gate tests — plus office-hours-forcing-energy (LLM-judge posture score; its sibling was already demoted) and cso-full-audit (250s/$0.57; the targeted cso tests stay gate). Saves ~$8-12 and 10-15 min per gate run. The plan-*-finding-floor tests stay gate deliberately: cheap insurance on the most-edited skill surface. Housing files have no whole-file self-gates, so the runtime E2E_TIERS filter handles both tiers; tier-alignment tripwire green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: judge default Sonnet -> Haiku 4.5 (D1a) The 25 doc-quality judges are rubric-scoring calls — a duty Haiku is already proven at in this repo (pty hung/working classifier, first-task-scaffold, hermetic-canary). Tests needing a stronger judge pass a model explicitly. Note: eval-store judge costs were hardcoded synthetic (0.02), so no baseline distortion. Re-baselined by the periodic run in this branch's final verification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: SDK runner default Opus -> Sonnet (D1a) agent-sdk-runner defaulted to Opus 4.7 while session-runner (the claude -p path) defaulted to Sonnet — an inconsistency between the two runners, not a decision anyone made. Unpinned tests were implicitly asserting the expensive model. The 30+ tests that genuinely need Opus already pin it via opts.model. Re-baselined by the periodic run in this branch's final verification; regressors get explicit Opus pins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CLAUDE.md tells the truth about the free suite; make-pdf gate is macOS-only The '<2s' claim was off by two orders of magnitude (measured 454s serial at v1.63; ~90-100s now under the parallel runner), and the bare 'bun test' guidance walked the whole repo, loading paid eval files and missing the strict classifier. Commands now say 'bun run test' with real numbers, document the strict-output invariant, the EVALS_JOBS / EVALS_CONCURRENCY split, the computed detach-timeout floor, and the required free-tests lane. make-pdf-gate drops its Linux leg (redundant with the free lane running make-pdf tests on every PR); macOS rendering coverage stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: catalog ratchet reads committed content; SDK unit pins follow D1a default Two follow-ups from the verification runs: 1. skill-size-budget's catalog estimate still flaked under --parallel (8356, then 8041, vs 4177 solo) even after filtering to tracked skills: sibling workers REGENERATE real SKILL.md files mid-run, so any live-tree read is a moving target. The ratchet now reads each tracked skill's frontmatter from git show HEAD: — the catalog that ships — which no concurrent worker can perturb. 2. agent-sdk-runner unit pins asserted the old Opus default through the default-flow fixtures; flipped to the Sonnet default (the explicit- override pass-through pins keep Opus — that path is unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): SIGKILL abandoned Chromium on close-race timeout (suite wedge) close()'s launched-mode path raced browser.close() against 5s and on timeout ABANDONED the child: this.browser nulled, process handle lost, Chromium alive holding keep-alive connections into test servers whose stop() then waits forever. Reproduced twice as an intermittent (~50%) whole-suite wedge — a 44min 0.1%-CPU hang pinned by a leaked LISTEN socket, and a 400s hang with commands.test.ts teardown in flight. The child handle is now captured BEFORE the race and SIGKILLed on race-timeout (launched mode only; headed keeps context.close). Race timers are unref'd so a successful close stops pinning the caller's event loop for the window. The four browse test servers force-close keep-alives (stop(true)) as belt-and-braces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: delete two dead-architecture security contract tests browse/test/security-source-contracts.test.ts and sidebar-security.test.ts read browse/src/sidebar-agent.ts at module scope — a file deleted (on main too) when the sidebar chat-queue path was ripped in favor of the terminal PTY. Both files have errored on load ever since: the old truncating suite never surfaced it, and no CI lane ran them. Their subjects (queue-spawn canary injection, preSpawnSecurityCheck, queued args, chat system prompt) no longer exist; server.ts retains processAgentEvent only in a comment. Live security coverage continues in security.test.ts (canary/verdict), content-security.test.ts (L1-L3), server-sanitize-surrogates.test.ts, and the security-bench suite. If the terminal-agent path should inherit any of the deleted contracts, that is a separately scoped piece of work against the component that actually exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(redact): calibrate placeholder recognition for code and doc shapes Three pushed-secret false positives blocked this branch's push; each is now recognized as a placeholder in the url_with_password/basic_auth_url validators, with real passwords still blocking (all pinned): - ${camelCase} JS template interpolations (the old check only skipped uppercase env-style ${DB_PASS}, so the supabase-provision bash->TS port's `postgresql://${dbUser}:${dbPass}@...` flagged as two pushed secrets). - The literal PASSWORD/pass placeholder in URL-format doc comments. - The provision lib's doc comments now use <PASSWORD>/PASSWORD forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: opt-in gate for live-playwright ML tests; ios-qa build hygiene security-live-playwright's L4 tests dlopen onnxruntime inside a bun --parallel worker whenever the dev box has a warm model cache — the source of the intermittent 'panic: Segmentation fault' + crashed-worker retries (and likely the residual run wedges). Same SECURITY_BENCH=1 opt-in as security-bench.test.ts; the L1-L3 tests in the file still run everywhere. Also: gitignore the ios-qa gen-accessors-tool Swift .build/ output (a side-effect of running its tests that kept polluting git status) and commit its Package.resolved so tool builds resolve reproducibly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: free runner output contract — name the failure, quiet the noise Diagnosing a red run used to mean re-running with output captured to a file and grepping past ~1000 lines of tab-close spam and ASCII art — several runs today ended with no way to even NAME the failing test, and a wall-timeout kill said nothing about which file wedged. New contract: the full child stream ALWAYS lands in a per-run log file (path printed up front); the console shows only runner lines, (fail) results, crash markers, and the terminal summary (--verbose restores the firehose; the strict classifier consumes the full stream in every mode). After every run a stable epilogue names the outcome: [test:free] FAIL — k failing test(s) in j file(s), c crashed worker(s). Full log: <path> ✗ <file> — <test name> ⚠ crashed+retried: <file> ⏱ in flight at kill: <files> (timeout only — the wedge suspects) Attribution rides bun --parallel's per-file output grouping (ANSI-stripped — color codes defeated a plain grep today). 12 new pins: epilogue formats, crash surfacing, quiet/verbose console policy, log completeness, in-flight-at-kill on a real hang. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: quarantine 5 pre-existing env failures individually (receipts in-file) Three snapshot tests (stale-ref error, snapshot -D diff, annotation cleanup) and two extension-sender-auth behavioral tests fail identically on origin/main v1.64.1.0, solo, on dev machines — verified per the blame protocol. Main's CI lane skip-lists both FILES wholesale; quarantining only the five failing tests keeps the other 60 guarding. Each carries the un-skip condition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: stealth-webdriver launch gets parallel-load headroom (120s) Playwright's default 30s launch timeout dies under the full-suite --parallel run when ~400 workers contend for Chromium launches — bun reports the hook death as an '(unnamed)' 30006ms failure (named on sight by the new runner epilogue). Both launch sites get explicit 120s timeouts; the runner's external wall-clock still bounds the ceiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: free-runner wall timeout 15min -> 6min (faster wedge diagnosis) The suite completes in ~100-160s; a wedge used to mean 15 minutes of silence before the kill-and-name epilogue fired. 6min keeps ~3.5x headroom over the slowest observed clean run while naming wedge suspects in minutes. --wall-timeout <secs> overrides per run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: run worker-hostile files in a serial child (first entry: security-live-playwright) The residual full-suite wedge, named by the new epilogue: Bun 1.3.13 segfaults running browse/test/security-live-playwright.test.ts in a --parallel worker ('panic: Segmentation fault ... a bug in Bun'), and the crashed-worker retry then wedges the whole invocation past the wall clock. The file passes serially. New WORKER_HOSTILE placement list: full-suite mode excludes listed files from the parallel invocation and runs them in their own strict-classified serial child afterward — execution placement, not a skip; each entry carries its reason and removal condition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: gate compare-board's file-level hooks too — the intermittent staller Skipped describes do NOT skip file-level hooks: the quarantined compare-board file still ran its top-level beforeAll (PNG fixtures + Bun.serve + a BrowserManager launch — exactly the 'needs a display-shaped env' code) on every run, and under parallel load that setup wedges. Caught red-handed by the runner's in-flight-at-kill epilogue: '⏱ in flight at kill: browse/test/compare-board.test.ts'. This was the suite's intermittent staller. Hooks now honor the same GSTACK_COMPARE_BOARD_TESTS gate; the gated file drops from 3.7s of live setup to 0.4s of pure skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: full suite runs as N shard processes; scrub spec-sync child env Two fixes from the wedge-hunt endgame: 1. Full-suite mode switches from one 'bun test --parallel' invocation to N concurrent shard PROCESSES, serial within each (the paid runner's proven model; N = min(6, cpus-2)). The single-invocation strategy hit three distinct Bun 1.3.13 worker pathologies in one day — a segfault whose crashed-worker retry wedged the run, a quarantined file's still-running file-level hooks stalling a worker, and spawn-heavy files hanging under load — and each one stalled the WHOLE invocation. Process shards isolate any wedge to its own shard. First full run under this model: no wedge, six epilogues, one real failure named. WORKER_HOSTILE stays as the paper trail; --parallel remains available per-shard for a future Bun. 2. That one real failure: spec-template-sync regenerates SKILL.md via a child that inherited the shard process's env — an earlier test's GSTACK_*/GBRAIN_* mutations changed generator output (failed in-suite, passed solo on an identical tree). The child now gets a scrubbed env: generator output must be a function of the templates, not of whichever test ran before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: tree-mutating tests run after the parallel shards; scrub relink env The flake family's root cause, finally: five test files REGENERATE shared repo artifacts in place (catalog-mode-full rewrites every SKILL.md in full-catalog mode; spec-sync and idempotency regenerate all skills; gen-skill-docs and skill-validation rewrite .agents/). Any concurrent shard reading those files sees a moving target — this one family produced the exactly-doubled catalog estimate, the golden-file drift, and the spec-sync mismatch chased earlier today. Full-suite mode now runs TREE_MUTATING files in one serial shard AFTER the parallel shards complete; CI's matrix is unaffected (per-runner checkouts). Also: relink's run() helper spread process.env into its children, so a sibling file's leaked GSTACK_HOME made the 'fresh install' test see a neighbor's skill_prefix. GSTACK_HOME is now dropped unless the test passes it explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: gbrain-detection-override joins TREE_MUTATING (mutator #6) It regenerates SKILL.md in place with --respect-detection (the gbrain variant adds ~1-3KB per carved skeleton) and git-restores afterward — its own header documents the approach. During that window the parity suite in a concurrent shard read inflated skeletons and failed 4 caps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: tree-ratchet readers join the serial phase (quiet tree by construction) Two consecutive runs failed the parity caps with byte-identical inflated skeletons (+~2KB gbrain-variant blocks) while the tree was clean before and after — some concurrent regen window keeps escaping the mutator census. Rather than hunt every present and future mutator, the tests that MEASURE the shared tree (parity caps, size budgets, carve guards) now run in the serial phase after the parallel shards: a quiet tree by construction, immune to any regen we haven't found. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * evals: judge default back to Sonnet — Haiku regressed the rubric family (A/B receipts) The partial-diff rehearsal was the Haiku judge default's first live run and it failed all three selected doc-rubric judges. Controlled A/B on the identical health-rubric prompt: Haiku 2/2/2 vs Sonnet 4/3/4, both with coherent reasoning — Haiku is simply a harsher grader on long-document rubrics, and every >=4 threshold in skill-llm-eval was calibrated against months of Sonnet baselines. Per D1a's pin-on-regressors protocol the default reverts; a new GSTACK_EVAL_MODEL_JUDGE override makes future recalibration a one-var experiment. Haiku keeps the classifier-grade duties (pty hung/working, warmup, distill via lib/eval-model.ts) and D1a's capture->Sonnet stands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test-runner): per-origin classifier buffers — interleaved pipes can't shear lines stdout and stderr are independent pipes; a chunk from one can arrive between two halves of a line from the other. The single shared pending-buffer glued those fragments into garbled lines: a sheared (fail) line went uncounted (defeating the exit-0-with-failures backstop) and a sheared terminal summary read as truncation. Counters stay shared; line assembly is now per-stream, and both runners tag the stream origin. Also drops the dead ChildProcess type import left by the killProcessGroup move. * fix(test-runner): real carve-guard keys in TREE_MUTATING; census pins; size-scaled wall deadlines TREE_MUTATING listed 'test/carve-guard-checks.test.ts' — a file that has never existed (the real ratchet readers are carve-guard-completeness and carve-section-ordering), so the intended serialization was silently absent. New census pin tests fail on any key that doesn't name a real free test file, and on a TEST_ROOTS entry that stops contributing files. Full-suite wall deadlines now scale with shard size (max(6min, files x 5s)) so a jobs=1 machine or the ~130-file Windows shards can't false-timeout a healthy run; explicit --wall-timeout disables scaling. Stale --parallel wording in the dry-run message, jsdoc, and the TREE_MUTATING ordering comment corrected to the shipped process-shard model. * fix(evals): selection under-selection fixes — duplicate keys, self-paths, quotePath Three under-selection holes: (1) duplicate E2E_TOUCHFILES keys (ship-plan-completion/-verification) — JS keeps the LAST duplicate, so the earlier dep lists were dead; pair deleted and a duplicate-key scan added to the literal-only tripwire. (2) The five rehomed e2e files didn't list themselves in their own dep lists, so editing the test never selected it. (3) git C-escapes non-ASCII paths without core.quotePath=false, so an accented filename matched no glob and deselected its tests. Also updates the stale --retry cost comment. * fix(evals): destructive-actions guard actually inspects Bash commands The rehomed guard filtered on typeof input === 'string', but session-runner records tool inputs as objects ({command} for Bash) — the filter matched nothing and the assertion could never fail, even against a real 'git push'. Now extracts the command from the object shape, same as the usedGitDiff check above it. * fix(redact): interpolation allowance can't swallow a real $word password The placeholder calibration used optional braces on both sides, which also suppressed bare $lowercase — a real password starting with '$' would have passed the HIGH gate. Interpolation now means ${identifier} (braced, any case) or bare $UPPER_SNAKE only; both connection-string patterns share one validator so they can't drift. Pins added for the bare-$word block, $UPPER allowance, and mismatched-brace block. * fix(gbrain): wait --timeout validates up front instead of polling forever on NaN Number('abc') is NaN, NaN comparisons are always false, and the poll loop never hit its deadline — an infinite 5s loop where the bash predecessor errored immediately. die(2) at parse time, with a test. * ci: least-privilege tokens on the two lanes that execute PR-controlled code free-tests runs PR code (install lifecycle scripts + the suite) with whatever the repo-default GITHUB_TOKEN grant is, persisted into .git/config by checkout. Now: permissions contents:read, persist-credentials false, pinned by the wiring test. actionlint gets the same treatment plus a digest pin on the third-party Docker Hub image (a tag is repointable with no GitHub-side audit trail, and the image sees the mounted checkout). restore-keys added to both caches so a lockfile bump warms from the previous cache; stale --parallel header wording corrected. * test(browse): unit coverage for the close() SIGKILL fallback The wedge fix (capture the Chromium child before the close race, SIGKILL on timeout) shipped without a test of the branch it added — the coverage audit flagged it as the diff's one regression-gap. The 5s race window becomes an injectable closeRaceMs field, and four unit tests pin: SIGKILL on hang, no SIGKILL on clean close, no SIGKILL on an already-exited child, SIGKILL on a rejecting close. * docs: CLAUDE.md describes the shipped shard-process model, not the abandoned --parallel probe * fix(test-runner): cancellation terminates the run; win32 kills the whole tree Installing SIGINT/SIGTERM forwarders suppresses Node's default terminate-on-signal, so a cancelled run killed the current child and kept LAUNCHING shards — observed as paid runs continuing to burn API spend after Ctrl-C (codex adversarial, repro'd ALIVE_AFTER_SIGTERM). The first signal now also schedules the parent's own exit after the children's SIGKILL grace, and both shard pools consult isTerminationRequested() before taking new work. On win32, killProcessGroup uses taskkill /T /F — detached:true creates no killable group there, and a bare child.kill orphaned every grandchild (ports, locks, and the inherited pipes that kept close from firing). Also: the tree-mutating serial shard prints dirty generated artifacts when it dies mid-regeneration, and --shard CI-matrix mode gets the same size-scaled wall deadline as full-suite mode. * fix(evals): preflight fails fast on spawn error, timeout, and exit 127 The ping only grepped stdout for two connection strings — a missing claude binary, a 30s timeout kill, or command-not-found all returned 'ok', and the fleet then burned ~30 shard timeouts discovering the outage one child at a time. Cross-model finding (testing specialist + codex adversarial). Other non-zero exits stay deliberately fail-open: a flaky preflight must not block a runnable suite; pinned both ways. * fix(redact): lowercase 'password'/'pass' at the URL-password position blocks The case-insensitive placeholder words waved postgres://admin:password@host through the HIGH gate as a doc placeholder (codex adversarial, verified zero findings pre-fix). URL-password position is now stricter than generic placeholder detection: ALL-CAPS doc convention (USER:PASSWORD), ${identifier} interpolations, bare $UPPER_SNAKE, and structural shapes (<your-password>) suppress; lowercase dictionary words block. Pinned in both directions. * fix(gbrain): DSNs percent-encode the password; body reads retry; stdout drains Three codex-adversarial findings in the provision port: (1) raw DB_PASS interpolation — a reserved character (/ # ? % @) restructured the URI, provisioning succeeded, and every consumer then failed to parse the DSN (unusable billable orphan); now encodeURIComponent, round-trip pinned. (2) await res.text() sat outside the transport try — a server that sent headers then reset the stream was an uncaught exit 1 instead of a retry-then-exit-8. (3) The bin entrypoint called process.exit() after unawaited stdout writes, truncating piped JSON; exitCode lets writes drain. * fix(evals): selection-path helpers join GLOBAL_TOUCHFILES; base-branch keys self-register The three-file split moved test-selection.ts into the globals but dropped the facade — an edit to test/helpers/touchfiles.ts (executable selection-path code imported by every consumer) selected ZERO paid tests, the exact invisible-non-execution class this branch exists to kill (claude adversarial, finding 1). e2e-helpers.ts (the harness every paid test imports) and paid-test-set.ts (paid-vs-free classification) had the same gap. The review/ship base-branch keys also register test/skill-e2e-review-attribution.test.ts so editing those tests selects them. * ci(free-tests): PR-number concurrency, failure-log artifact, main-push runs Three red-team/adversarial findings on the new required lane: (1) concurrency keyed on bare head_ref — two forks with the same branch name shared one group, so a push to fork B cancelled fork A's in-flight REQUIRED check (merge-pipeline DoS with no code fault); key on the PR number. (2) The runner's full logs die with the runner in os.tmpdir() — a red check named WHICH test failed but never why; upload the shard logs as an artifact on failure. (3) PR-only trigger meant two individually-green PRs could merge into a red main with nothing running the suite there; add push: branches: [main]. * ci: PR-number concurrency keying on the eval and Windows lanes too Same fork-branch-name collision as free-tests.yml: bare head_ref carries no owner prefix, so same-name branches from different forks shared a cancel-in-progress group. * test(evals): retro E2E passes require the report on disk Both retro tests passed with zero work product: error_max_turns counted as success and the content assertion was guarded by fs.existsSync — a run that burned 30 turns and wrote nothing recorded green (red team). The report is now load-bearing for pass/fail. * chore: bump version and changelog (v1.66.0.0) Test/evals/CI speedup pass: release summary + itemized changes in CHANGELOG.md; TODOS.md marks the free-suite exit-code P1 complete and files the review-army follow-ups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(redact): fully-braced ${...} interpolations are code, whatever they contain The identifier-only braced form flagged the DSN builder's own ${encodeURIComponent(dbPass)} call site as a pushed secret — a scan that cries wolf on the fix for the previous finding. Any ${...} spanning the whole password segment is template code; bare $word stays uppercase-only so $hunter2 still blocks. The mismatched-brace negative fixture assembles at runtime so this file's own pushed bytes carry no blockable URL shape. * test(gbrain): assemble the pooler expected-URL from parts (scan-clean pushed bytes) * docs: sync docs for v1.66.0.0 (test/evals/CI speedup) CONTRIBUTING.md, AGENTS.md, and ARCHITECTURE.md still taught bare `bun test` for the suite; the shipped runner deprecates it (walks the whole repo, loads paid eval files, misses the strict classifier). All suite-level references now say `bun run test`, the Tier 1 section describes the strict shard runner (~90-100s, --verbose, --wall-timeout), the sharded paid-runner paragraph documents diff-based shard skipping and the EVALS_JOBS / EVALS_CONCURRENCY split, the Tier 3 row points at the actual judge-only invocation, and GSTACK_EVAL_MODEL_JUDGE is documented at the judge it overrides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(free-tests): restore the PR-number concurrency + failure-log artifact; truth-fix stale comments The workspace-revert incident that hit CHANGELOG/TODOS mid-ship also caught free-tests.yml between edits: commit 8d6c2ff8's message claims PR-number concurrency + artifact upload + main-push runs, but only the push trigger survived to the commit (caught by the /document-release doc-vs-code audit). Both re-applied. Also: eval-model.ts header said capture defaults to Opus (it's Sonnet per D1a), paid-shards' header pinned a stale 44/63 shard census, and two CHANGELOG phrases over-claimed ('six' -> 'up to six' shard processes; retry-1 scoped to retry-bearing paid paths). * ci: setup-buildx before every cache-exporting image build First live run of the cache trio failed at flag-parse time: the default buildx `docker` driver hard-errors on cache-to registry export ('Cache export is not supported for the docker driver'), which failed build-image on PR #2593 and skipped the entire gate eval matrix behind it. docker/setup-buildx-action creates the docker-container builder that supports registry cache export; all three build sites (evals, evals-periodic, ci-image) get it. * fix(browse): Xvfb identity is argv[0]'s basename, not a cmdline substring First Linux CI run: isOurXvfb identified the TEST RUNNER as our Xvfb — the suite's own argv contains 'xvfb.test.ts', the substring match over the whole cmdline passed, and the start-time check matched because the pid was real. Any process whose ARGUMENTS mention xvfb (a runner, an editor) was killable — the sibling-kill class the identity check exists to prevent. Identity now rests on argv[0]'s basename ('Xvfb'), with a sh-$0 regression pin. isDisplayFree falls back to the X socket/lock files when xdpyinfo isn't installed (x11-utils is absent on some images that ship Xvfb). * fix(test-runner): strip GHA ::group:: wrappers before file attribution On GitHub Actions bun wraps each file's log section in ::group::. The un-stripped header failed FILE_HEADER_RE, failures attributed to the PREVIOUS file, and the terminal recap's re-printed (fail) lines landed under a phantom second file — the first Linux run reported 5 real failures as 10 across 2 files (one of them innocent). Strip the prefix before matching; the existing file+test dedupe then absorbs the recap. * test: first-Linux-run environment fixes — bun-only PATH shim, claude gate, darwin-scoped pdf gates Three environmental assumptions the Linux lane exposed: (1) gbrain-detect's deterministic SAFE_PATH lacked the bun runtime, so every env-shebang spawn exited 127 on CI; a scratch dir holding ONLY a bun symlink joins the PATH (appending bun's real dir would leak its siblings — dev boxes keep gbrain there too). (2) host-config's 'detect finds claude' assumed a claude binary; the secretless lane deliberately has none — gated on Bun.which. (3) The four make-pdf render gates hard-required prerequisites on ANY CI, but the make-pdf gate workflow is macOS-only by decision and the Linux lane doesn't build dist/pdf — hard-require scoped to darwin. * ci(free-tests): run the suite under xvfb-run Headed-browser tests (handoff, extension sidepanel DOM) need a real DISPLAY; the first Linux run died on Playwright's 'headed browser without an XServer' banner. xvfb-run -a provides the display; x11-utils ships xdpyinfo for display probing. * fix(test-runner): bun's headerless failure recap can't invent a phantom failing file Round-3 CI showed the remaining half of the recap bug: bun prints 'N tests failed:' then re-prints every (fail) line with NO file headers, so they attributed to the stale currentFile — an innocent file (test/uninstall.test.ts) was charged with another file's 5 failures. The recap marker now ends attribution (currentFile=null, chunk closed) and recap re-prints of already-recorded test names dedupe; a recap-only failure the main run never attributed still records, unattributed, as belt and braces. * test(browse): sidepanel DOM suite launches with --no-sandbox on CI + console capture The suite's raw chromium.launch had no --no-sandbox — every browse test that goes through gstack's launcher (which always passes it) survived the Linux lane, while this file's sandboxed renderer died on first navigation: waitForFunction hung to the 15s test timeout, then every newContext failed with Target.createBrowserContext. Also wires pageerror/console-error capture at all six pages so a page-side failure reads as itself in CI logs instead of a bare timeout. * test(browse): delete the sidepanel security-DOM suite — it tests UI removed in v1.14 Another member of the never-ran class: the file skipped everywhere (Playwright chromium absent locally, no Linux CI until this branch), so it rotted invisibly through THREE contract changes — the v1.63 /extension-token bootstrap, the endpoint growth (/memory, /pty-session, /sse-session), and finally the v1.14 sidebar-REPL rewrite that removed the security shield/banner UI it asserts on (#security-shield survives in sidepanel.html as a dead hidden stub with no JS driver; sidepanel.js:87 and :1317 document the removal). The Linux lane executed it for the first time and it can never pass: the behavior is gone. The L1-L3 security filters it name-checked stay covered by the ~83 unit/behavioral security tests. The free-tests lane also vendors xterm assets (bun run vendor:xterm) so the sidepanel terminal scripts load for any future DOM coverage. * ci(evals): per-row retry override — two receipted rows keep the third attempt Three PR rounds of receipts: pty-plan-smoke failed attempt 2 in two consecutive rounds with ROTATING members (plan-design-review, then plan-eng-review) and e2e-workflow's document-release timed out on attempt 2 in round 4 — while both families pass on branches still running three attempts, and every other row stayed green at --retry 1 across all rounds. Matrix rows gain an optional retries field (default 1); only these two rows set 2, keeping the measured retry-amplification win everywhere else. * test(windows): curate the seven POSIX-bound files the expanded lane surfaced; fix flag-utils path embedding First full run of the expanded Windows lane (13 -> ~258 files, PR #2593 run 31918591602) failed in exactly 8 files. One was a real test bug, fixed: design-flag-utils embedded a raw Windows ROOT into a bun -e string where backslashes act as escapes (D:\a\gstack imported as D:agstack) — forward slashes work on every platform. The other seven are POSIX-bound in ways the content patterns cannot see (sed/ln/bash ARE their subject, a shebang shim arrives via variable, wall-clock retry bounds on the slowest runner) — each gets a receipted KNOWN_WINDOWS_INCOMPATIBLE entry, and the census pin now covers that list so a renamed file fails the suite instead of silently keeping a stale exclusion. * test(windows): curate skill-census + browser-manager-unit; surface unhandled errors in the epilogue Round-2 Windows census (zero failing TESTS — the first curation wave held): shard 1 failed on an unhandled module-load throw in skill-census (the skills-tree symlink layout needs Developer Mode CI runners lack) and shard 2 wedged to its wall deadline inside browser-manager-unit — both get receipted exclusions; macOS + Linux lanes keep covering the files. The unhandled-error class also exposed an epilogue gap: it fails the shard via the strict classifier but produces no (fail) lines, so the epilogue read 'FAIL — 0 failing test(s)' with no culprit. The reporter now attributes each '# Unhandled error between tests' marker to its chunk and the FAIL line carries the count. * docs: file the two Windows-lane follow-ups (browser-manager wedge, skill-census symlinks) * test(windows): round-3 curation — seven files the round-2 wedge had been truncating The browser-manager-unit wedge was cutting shard 2 short, so each Windows round revealed the next segment of never-run files. With the wedge excluded, shard 2 completes (50s) and shows its real failures: seven more POSIX-environment files (PID/cmdline identity probing, bash scripts as the subject under test, env-scrubbed bun spawns). Shards 1 and 3 (including all tree-mutators) now PASS on windows-latest — this should be the fixed point: ~234 files of real Windows coverage vs the 13 hand-picked before. * test(windows): round-4 curation (spawnSkill env, symlink fixtures) + shard-log artifact Shard 2 ran all 132 files with zero (fail) lines yet bun exited 1 — unhandled errors in a shape neither counter names, and the Windows lane had no log artifact to attribute them. Statically attributed and excluded: browser-skill-commands (spawnSkill spawns bun with a constructed env; resolution fails under Windows spawn) and security-audit-r2 (evil-link symlink fixtures need Developer Mode). The lane now uploads its shard logs on failure like free-tests.yml, with os.tmpdir() pointed at runner.temp so the glob can find them. * evals: Opus pin on the spec AUQ-matrix entry — D1a regressor, receipts in-file The periodic re-baseline for the capture default (Opus -> Sonnet) found exactly one regressor across the seven-entry AUQ behavioral matrix: spec failed twice under Sonnet ('never reached a question in budget', 242s) while its six siblings passed; the controlled Opus re-run passed cleanly (7/7 format, substance 5, 160s), and a second run through the new per-entry model plumbing confirms. MatrixSkill gains an optional model field wired into captureFirstAuq; only spec sets it. TODOS gains the re-baseline receipts for the never-baselined periodic tail (three setup-gbrain files + ship-idempotency, all local-only). * test(evals): scope-gate assertion carries its evidence tail; file the detector-flake TODO The plan-design-review member fails ONLY scopeGateQuestionObserved intermittently on unchanged code (PR #2593: red rounds 3/11 + rerun, green rounds 5/6 — every attempt terminal, no plan-mode leak), and a bare Expected-true/Received-false is undiagnosable from CI logs. The check now throws with the last-2KB visible evidence, so the next failure distinguishes a detector-sensitivity miss from a real silent bypass. TODO filed with the full receipt trail. * test(evals): review-dashboard-via budget 300s -> 360s — third ratchet of the same contention story PR #2472 documented the 180s deterministic 0-turn startup timeouts and ratcheted to 300s; PR #2593 hit 302s timeouts on attempt 2 in two consecutive runs while five sibling rounds passed — marginal at 300s under 40-way in-shard concurrency. Same headroom its contention-class sibling (retro-base-branch) carries; outer bun timeout rises to 480s. * test(evals): document-release budget 180s -> 300s — same contention ratchet, receipts in-file Timed out at exactly 180s on its final attempt twice on PR #2593 (rounds 4 and 13) while passing four other rounds — a 30-turn multi-step doc workflow is marginal at 180s under 40-way in-shard CI concurrency. Same story and same fix as review-dashboard-via and retro-base-branch; outer bun timeout rises to 360s. * docs: file the systemic in-shard-concurrency follow-up behind the timeout-flake family --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The browse server dies between Bash tool invocations in Claude Code. Every new
$Bcommand starts a fresh server, losing all cookies, page state, and navigation. This makes multi-step QA testing impossible.Root cause: Two mechanisms kill the server when the parent Bash shell exits:
SIGTERM from sandbox: Claude Code's Bash tool sends SIGTERM to all child processes when a command completes. The server's SIGTERM handler called
shutdown().Parent PID watchdog: The server polls
BROWSE_PARENT_PIDevery 15s. When the parent shell dies, the watchdog calledshutdown().Fix
shutdown(). Explicit shutdown still available via/stopor SIGINT.shutdown(). Idle timeout (30 min) handles cleanup.What doesn't change
/stopcommand and SIGINT still work for intentional shutdownRepro (before fix)
After fix
Verified across 5+ separate Bash calls maintaining cookies, form fields, and navigation.