Commit Graph
25 Commits
Author SHA1 Message Date
+22 8bb6d65270 v1.91.66.0 fix: /autoplan without permission prompts after background reviewers, 34 contributor PRs from the backlog triage (#3087)
* docs: fix CONTRIBUTING.md test-tiers link in TESTING_INTERNALS

The link resolved relative to docs/, pointing at a nonexistent
docs/CONTRIBUTING.md. Adopts #3070.

Co-authored-by: thearbajonart <334330110+thearbajonart@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci: refresh GitHub Actions SHA pins (setup-buildx 4.4.1, build-push 7.4.0, attest 4.2.2, setup-node 7.0.0)

Rewrite of dependabot #2988. Every bumped `uses:` stays pinned to the full
commit SHA (verified against the upstream tags) and now carries its exact
version comment, including the lines that had none. setup-node 6.5.0 -> 7.0.0
is a major bump; its notes list an ESM migration, cache outputs and removal of
a dummy NODE_AUTH_TOKEN export, none of which affect our node-version-only use.
actionlint 1.7.11 (CI image) and 1.7.12 are clean.

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(openclaw): stop advising a per-spawn env for spawned sessions

OpenClaw's sessions_spawn has no env argument (openclaw/openclaw#51654
was closed without adding one), so the documented
env: { OPENCLAW_SESSION: "1" } silently did nothing. Point users at the
ACP harness or Gateway environment instead and guard the old advice.
Adopts #2749.

Co-authored-by: DanCanadian <211164803+DanCanadian@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(benchmark): scope /benchmark routing to web page performance

/benchmark claimed the bare words "benchmark" and "performance", so model
and eval benchmark requests could route to the web performance skill
instead of /benchmark-models. Narrow the description, Use-when list and
triggers to web-specific phrases, point model/skill benchmarks at
/benchmark-models, and split the root router line the same way. The
catalog lead stays within the discovery budget ("Web performance ...").

Adopts #2302 and closes duplicate #2083 (rewritten on current main).

Co-authored-by: Nader Helmy <31414549+creatornader@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(llms.txt): link skills by generated path, drop host-skipped skills

gen-llms-txt built every skill link as <name>/SKILL.md, so the router
pointed at a nonexistent gstack/SKILL.md (it is the root SKILL.md) and
/claude-code pointed at a SKILL.md the Claude host never generates.
Links now use the template's generated output path, and skills the
Claude host skips are left out of the index. Adopts #2279.

Co-authored-by: Nader Helmy <creator.nader@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ci): install Bun from the SHA-256-verified release archive in GitLab CI and Ubicloud setup

Rewrite of #1713. .gitlab-ci.yml's .setup-bun anchor and
scripts/ubicloud/setup-free-suite.sh still piped https://bun.sh/install into
bash. Both now download the bun-v$BUN_VERSION linux-x64 release zip, check it
against the same SHA-256 Dockerfile.ci uses (#1706) before unzipping, install
it to ~/.bun/bin and assert `bun --version`. test/ci-image-cli-pin.test.ts runs
curlPipedToShell over both files and binds their hash and BUN_VERSION to
Dockerfile.ci's amd64 arm and package.json's engines floor, so a Bun bump has
to move all of them together. Verified by running the GitLab anchor in
debian:stable-slim (bash and sh) and the Ubicloud block in a temp HOME.

Co-authored-by: Zee Zheng <228040220+spacegeologist@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(browse): viewport auto unpins a pinned viewport without a restart

`viewport WxH` pinned the page with no way back short of `browse restart`,
which also drops a headed /connect-chrome session. `viewport auto` (aliases
`reset`, `unpin`) now undoes the pin:

- headed: the context already launches with `viewport: null`, but Playwright
  cannot drop a page's emulated size (and re-applies it on new frame
  sessions), so the active tab gets a fresh page in the same context at the
  same URL or load-html content. Cookies and storage persist, the tab id and
  ownership stay, refs reset as on navigation, and the page follows the real
  window again.
- headless: back to the 1280x720 launch default, tracked viewport included.

`auto` with --scale is rejected. Fixes #1059. Adopts #2782 and #1881.

Co-authored-by: Jack Thomas <17882510+loulanyue@users.noreply.github.com>
Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(browse): screenshot accepts --clip=x,y,w,h and --selector=<css>

Agent-generated commands often use the GNU `--flag=value` form, which threw
"Unknown screenshot flag". Both value flags now accept it, through the same
validation and conflict checks as the space-separated form. Adopts #1563.

Co-authored-by: techcenter68 <75930234+techcenter68@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(benchmark): price Codex cached input at the cache rate, stop calling Gemini OAuth ready

Codex reports cached_input_tokens as a subset of input_tokens, but the
adapter billed all of input_tokens at the full input rate. Split them
into disjoint uncached and cached counts (what estimateCostUsd expects)
and show new+cached input in the table and Markdown reports. A stored
Gemini OAuth file alone is now reported as not ready, with a reason
naming GEMINI_API_KEY; key-based auth stays ready. Adopts the focused
subset of #2283; the capacity error code and stdin change are left out
(no captured failure or reproduced hang).

Co-authored-by: Mattias Petersson <mattias.petersson@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review): catch loosened-input consumers and stale user-facing strings

The Enum & Value Completeness check fired only on newly added values, so a
diff that widens what a validator, parser or guard accepts, and breaks an
unchanged downstream consumer outside the diff, was a blind spot. Widen the
check's trigger (heading unchanged) and add a consumer-grep bullet. Teach
the maintainability specialist to flag user-facing strings whose guarding
condition changed while the text did not.

Adopts #2141 (rewritten on current main; the section heading is kept).

Co-authored-by: Arno V <38716+aversini@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(global-discover): bucket Codex sessions by originator

Every Codex rollout counted as one undifferentiated codex bucket, so
/retro global could not tell interactive Codex Desktop work from
codex_exec runs or Claude Code-driven calls. Normalize
session_meta.payload.originator into desktop, exec, claude_code or
other (missing, unknown and non-string values), report the split per
repo as codex_originators and globally as tools.codex.originators, and
print it in the summary. Existing totals are unchanged. Adopts #1488.
Refs #1315.

Co-authored-by: 0xDevNinja <manmit0x@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(hook-check): cover skill frontmatter hooks, sourced helpers, conflict markers and the caller's tsconfig

Rewrite of #3066. bin/gstack-hook-check only listed setup's hooks plus
/autoplan's by name, so the /careful and /freeze PreToolUse checks (also run
by /guard and /investigate), /plan-ceo-review's hand-off hook and the helpers
they source were never parse-checked before Claude Code ran them.

- The hook list is setup's _hook_command_path arguments plus every gstack path
  on a `command:` line in a generated */SKILL.md frontmatter (9 hooks, up from
  6). The generated files are read rather than the .tmpl sources because the
  /autoplan and /plan-ceo-review commands come from resolvers.
- Each shell hook's gstack-tree helpers (`.`/`source` lines, assignments and
  shellcheck source= directives, followed transitively) are parsed too:
  careful/bin/hook-extract.sh and bin/gstack-state-root.sh for /careful and
  /freeze.
- The shim, its helpers, each TypeScript entry and every local module the
  bundle used (bun build --metafile; entry only when bun lacks it) are scanned
  for conflict-marker lines, which still parse inside heredocs, strings and
  template literals.
- bun build runs from the gstack root, so the caller's broken tsconfig.json or
  bunfig.toml no longer fails every TypeScript hook. Lists are newline
  separated, so a root with spaces works.

Tests in test/setup-hook-syntax-gate.test.ts cover each case; the gstack-doctor
fixture now copies autoplan/SKILL.md, whose frontmatter registers the hook its
hooks-row test breaks. docs/troubleshooting.md describes the wider check.

Co-authored-by: Benjamin D. Smith <7134745+BenjaminDSmithy@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review): extend defensive-security framing to the security specialist and Red Team

The "authorized defensive-security review" framing and fixture summary mode
lived only in the adversarial subagent prompt. The security specialist and
the Red Team also reason adversarially over the full diff, so repos whose
test corpus is attack payloads could still get those passes refused.

Move both paragraphs into scripts/resolvers/defensive-review.ts (the
adversarial render stays byte-identical), state them once in the specialist
dispatch for the security specialist and the Red Team, and have the Red
Team prompt start from them. Testing and the other specialists keep full
fixture reads; the codex host still strips Review Army. Parity caps for
ship and review rise by the measured 1,380 bytes each (carve-guards notes).

Adopts #1921 (rewritten on current main).

Co-authored-by: Brian Majewski <1693720+bmajewski@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(team-init): make the required-mode hook shell-neutral (node CommonJS, no shell variables)

Rewrite of #2217. Required team mode wrote a bash check-gstack.sh and
registered it as "$CLAUDE_PROJECT_DIR/.claude/hooks/check-gstack.sh", which
only works when the hook runner is a POSIX shell. Under cmd.exe or PowerShell
(Windows, Copilot CLI) the variable is not expanded and the .sh cannot run, so
an intended allow or deny became a hook error.

- The hook is now .claude/hooks/check-gstack.cjs: the same root search
  (GSTACK_ROOT, every host's global root, the migrated repo), with the home
  directory from HOME, then USERPROFILE, then os.homedir(). It prints {} when
  gstack resolves and the PreToolUse deny JSON with exit 2 when not.
- The registered command is `node -e "require(require('path').resolve(
  process.env.CLAUDE_PROJECT_DIR || '.', '.claude', 'hooks',
  'check-gstack.cjs'))"`: no `$` or `%`, so sh, cmd.exe and PowerShell all
  pass it through unchanged.
- An existing check-gstack registration (matcher Skill or Skill|skill) is
  replaced in place; duplicates are dropped and unrelated hooks in the same
  entry kept. The legacy .sh is deleted only after the settings write
  succeeds (a tracked one joins the suggested git add), and is kept when bun
  is missing or the write fails, which now reports instead of claiming success.

Tests: team-mode and the hook-schema test run the command as registered;
test/team-init-hook-shell-neutral.test.ts runs it through the platform's
default shell (cmd.exe on the Windows lane, which curates it in) and through
PowerShell when installed (verified here in the pwsh 7 container). The
routing-probe check for the bash `_GSTACK_ROOT` gate now pins the same
contract in the .cjs: every host root probed, block only when none resolves.

Co-authored-by: neallee2012 <1499497+neallee2012@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(setup-gbrain): register the gbrain MCP through bun.exe on Windows so no console window opens

Rewrite of #2007, folding #1963's MCP notes. On Windows the local stdio
registration stored the bun `gbrain` binstub; gbrain.exe starts bun in a new
console, so every Claude Code session opened a visible terminal for the MCP
(the contributor's process trees show it; bun.exe launched directly attaches
to Claude Code's headless ConPTY).

Under MINGW/MSYS/Cygwin the skill now resolves BUN_ROOT from BUN_INSTALL or
~/.bun and registers `$BUN_ROOT/bin/bun.exe <global node_modules
gbrain/src/cli.ts> serve`. That path also covers a `bun link`ed clone, whose
global node_modules entry links to it; gstack-gbrain-install records no other
clone location. When bun.exe or the entry is missing it registers
`$BUN_ROOT/bin/gbrain.exe` by its full name, because Claude Code does not
apply PATHEXT to a stored path (#1963). POSIX keeps the binstub line. The
skill also notes that `claude mcp list` can show "Failed to connect" while a
live session holds a local PGLite brain's single-writer lock (#1963).

test/setup-gbrain-windows-mcp.test.ts runs the generated fence under bash with
stub uname/claude and a temp HOME for MINGW64, MSYS with BUN_INSTALL, a
missing entry, and Linux. It is listed as Windows-incompatible (POSIX stubs);
the console behavior needs a manual Windows check.

Co-authored-by: Max Hogan <max@maxhogan.dev>
Co-authored-by: maxpetrusenkoagent <280464918+maxpetrusenkoagent@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(hosts): rewrite every "Agent tool" phrasing for agent-runtime hosts

The Agent-tool rewrite for hermes, gbrain, openclaw and factory keyed only
on "use the Agent tool", but templates mostly say "via the Agent tool",
"with the Agent tool", "multiple Agent tool calls" or "Use the Agent tool",
so the Claude tool name leaked into those hosts' skills and sections. Add
ordered rules after the existing key (possessive, "the Agent tool", then
bare "Agent tool") mapping to sessions_spawn (gbrain/openclaw),
delegate_task (hermes) and delegation (factory). No goldens change.

Adopts #1935 (rewritten on current main, without the new goldens).

Co-authored-by: katlun-lgtm <264247399+katlun-lgtm@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(setup-deploy): Vercel auto-deploys only when connected to the Git repo

/setup-deploy stated that Vercel always deploys on push, which is false for
CLI-only projects with no Git integration, and its nothing-detected path did
not say why such a project is invisible to detection. Qualify the Vercel
trigger (ask for and record the CLI deploy command otherwise) and note that
`vercel link` creates .vercel/ so a re-run detects the project.

Adopts the Vercel accuracy part of #2992 (rewritten on current main); the
third-party bring-up tool pointer is not adopted.

Co-authored-by: mikehasa <91774998+mikehasa@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(retro): persist recommendations so the next retro detects follow-through

/retro produced "3 Things to Improve" but never saved them in its JSON
snapshot, so the next retro could not tell whether last week's
recommendations were acted on. Step 13 now drafts the items before saving
and always writes a `recommendations` array ({category, text}, [] when
empty); Step 12 scores each prior recommendation addressed/partial/open
against this window's commits, files and metrics and renders a
follow-through line, skipping silently for snapshots without the key. The
report format ties the items to the saved array. Global mode is unchanged.

Adopts #1853 (rewritten on current main). Refs #1834.

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(global-discover): count the interactive Codex CLI (codex_cli_rs) in its own cli bucket

* feat(developer-profile): --reconcile backfills office-hours tenure from timeline.jsonl

sessions[] is written only at office-hours Phase 4.5, so runs that ended
earlier never counted toward tenure and the tier undercounted. Add a manual
`--reconcile` that scans projects/*/timeline.jsonl for office-hours
`completed` events and appends a count-only row ({date, mode, project_slug,
signal_count: 0, signals: [], backfilled: true}) for each run with no
sessions[] entry for the same project within 60 minutes, extended back by a
duration_s under 8h. It is idempotent, skips malformed lines, and leaves the
file untouched on a no-op. `--read` counts backfilled rows for SESSION_COUNT
and TIER but takes LAST_*, DESIGN_*, CROSS_PROJECT and the nudge from logged
sessions only. Not wired into the preamble yet; the office-hours greeting
guard for backfill-only profiles is left to the office-hours owner.

Adopts #2668 (rewritten on current main). Closes part (b) of #2657.

Co-authored-by: Briac <137724549+briacSck@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): the publication hook approves /autoplan's own Reads after the skill turn

Claude Code applies a skill's allowed-tools only to the turn that invoked it.
On 2.1.292 the default fork-subagent mode runs reviewers in the background, and
the completion notice starts a new turn, so later Reads outside the working
directory (the next phase's section file, the close packet) raised permission
cards. Skill hooks stay registered for the session, and a PreToolUse "allow"
skips the card (reproduced in a PTY on 2.1.292).

autoplan/bin/owned-read.ts approves a parent Read only when the guard itself
returned no output, and only for this installation's autoplan/sections/*.md
(canonical path or the gbrain :user render) or an immutable phase artifact in
<repo>/.gstack/tmp/autoplan/autoplan-<phase>-*/ whose repo is the session's
project directory or contains it. Denials, unverified allows, child calls and
every other path are unchanged. renderSectionBase and userRenderRoot move
there with it.

Tests that asserted a verified driver Read as {} now expect the approval;
autoplan-owned-read covers the shipped shim, the close packet without a
journal, verified artifacts through the guard, look-alikes (foreign install,
writable copy, symlink alias, other repository, other names) and a guard
denial staying a denial.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): restore point and phase artifacts live in the project's .gstack/tmp/autoplan/

Skill hooks do not run inside subagents, so a reviewer launched after a
background completion notice raised a card for its own input (native-prompt.md
under the state root). Step 1 now puts RESTORE_PATH in the repository's
git-excluded .gstack/tmp/autoplan/ (added to .git/info/exclude, mode 700), and
the snapshot tool writes every phase artifact beside it, so /autoplan and its
reviewers read them from the working directory without a card. The guard's
bindings are unchanged: they anchor on the restore point's directory. Step 1
still prints the slug, which the old RESTORE_PATH carried.

A free test runs the generated Step 1 block twice from a repository
subdirectory: the restore point lands in <repo>/.gstack/tmp/autoplan/, the
exclude line is written once and git status stays clean. The autoplan parity
cap rises by the measured bytes (115,270 / 101,979 = 1.1303).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(evals): autoplan-guard-pty fails on any permission card after the reviewer's notice turn

The scripted boundary now mirrors Step 1's layout (restore point and artifacts
in the project's .gstack/tmp/autoplan/) and continues past the Phase 2 entry to
dispatch the Phase 2 reviewer after the CEO reviewer's completion notice. The
case asserts four verified guard allows, that the close packet Read, the Phase 2
section Read and the Phase 2 dispatch came after a task-notification turn, that
the Phase 2 reviewer read its input, and that no permission card appeared at
all. Still one trial on haiku, about 75-85 seconds.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): permission cards after a background reviewer, and where /autoplan keeps its files

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(outside-review): read a trailing '— Medium.' or '(High)' severity on a finding line, the shape Codex now writes

* feat(design): honor OPENAI_BASE_URL for every design API call

One resolver, openaiUrl() in design/src/auth.ts, now builds every OpenAI
URL the design binary, its prototype and its live model check use. Setting
OPENAI_BASE_URL sends image generation and screenshot analysis to an
OpenAI-compatible gateway; unset or blank keeps api.openai.com. The
prototype script now goes through receiptedFetch, so its egress is
receipted like the rest, and receipts already record the gateway host.

A source scan keeps any design file outside auth.ts from hardcoding an
OpenAI API URL again, and a request-shape test proves the gateway reaches
both the vision and image endpoints.

Co-authored-by: JackJin <1037461232@qq.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* review: testing specialist treats negative assertions as decisions

A new assertion that something is absent records a behavior removal and
needs cited intent. A rename or removal in the diff can empty a standing
negative assertion so it passes forever; the specialist now greps the suite
for negative assertions naming the old value.

Adopted from the testing-specialist part of a community PR; the scope-check
hunk targeted text that no longer exists.

Co-authored-by: EmilianoU26 <314541067+EmilianoU26@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* review: security specialist checks authorization a route-guard read misses

Four access-control classes where the guard is correct and the object still
leaks: error pages composed outside the guard, ownership surviving a role
downgrade, list/detail/download paths checked as one, and a session mutated
before the identity-switch callback is validated.

Adopted to review/specialists/security.md only; the /cso hunk targeted a
bullet list that is now an OWASP/ASVS table.

Co-authored-by: EmilianoU26 <314541067+EmilianoU26@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* review: zero findings is valid; suppress six common false positives

The checklist now says a clean diff may come back clean, and suppresses
framework-owned error handling, long exhaustive switches, null derefs a
type guard already narrows, N+1 on fixed-cardinality loops, hardcoded test
fixture values and non-cryptographic Math.random().

The contributor's "missing input validation on an internal function" and
"missing await on fire-and-forget" suppressions are left out: both can hide
real bugs.

Co-authored-by: frosimanuel <frosimanuel@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(todos): parked contributor ideas by theme; /autoplan permission follow-ups

* design-review: capture code tokens before universal principles

/design-review read only DESIGN.md and otherwise fell back to universal
principles, so a project whose system lives in CSS custom properties,
tailwind.config or a theme file got audited and mocked up against an
invented aesthetic. The skill now reads code tokens before falling back,
and the target-mockup brief reuses the captured colors, fonts, radii and
spacing, changing layout only.

The contributor's optional microcopy checks (domain vocabulary, human
register) are not adopted here.

Co-authored-by: Tom Lam <tomlam0811@berkeley.edu>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(sidebar): add a terminal text-size picker to the toolbar

The sidebar terminal toolbar gets an XS-XL text-size picker beside
Restart. SM (13px) stays the default, so nothing changes until a user
picks a size; the choice persists in chrome.storage.local and applies
to the live terminal, re-fitting xterm and re-sending the PTY size two
frames later so the new character cell is measured first.

A VM test runs the real sidepanel-terminal.js through one auto-connect
and checks the creation size, the stored-size fallback, persistence and
the live resize.

Co-authored-by: Yotam <yotsurfing@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(design-shotgun): generate with Codex's built-in $imagegen on Codex

On the Codex host, /design-shotgun now generates and edits mockups with
Codex's documented $imagegen skill in its default built-in mode (the
image_gen tool), so users signed in with ChatGPT need no OPENAI_API_KEY.
The design binary stays for the local comparison board only, images are
published with gstack-design-claim as before, and screenshot evolution
loads the captured page with view_image before an $imagegen edit.

Step 3c-3d moved into a DESIGN_SHOTGUN_GENERATION resolver that returns
the existing design-binary text unchanged for every other host; the
Claude output differs only in two host-neutral rewordings of the
evolve-from-screenshot pointers. DESIGN_SETUP and the regenerate step of
the board loop gain matching Codex-only text for design-shotgun.

Co-authored-by: Jinzhe <hwang.phy@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* voice: ban the "load-bearing" stock phrase

Models overuse "load-bearing"; it joins the AI-vocabulary ban line in both
voice tiers. The tier-2 voice directive is also tightened (the meta
"compressed for runtime" tag and the bounded closer's restated exemption
clause) so the shared preamble shrinks by 163 bytes per tier-2+ skill,
which also pays for the reply-language rule that follows. The ship goldens
are refreshed for the new render.

Co-authored-by: Nader Helmy <creator.nader@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* voice: reply in the language of the user's latest message

Every skill's voice directive now tells the agent to reply in the language
of the user's latest message unless asked otherwise, so long English skill
text no longer drags a Chinese (or any non-English) session into English.
Mixed-language sessions follow the latest message. Code, commands, paths,
identifiers and quoted output stay verbatim, and at tier 2+ so do the
machine-read question markers (D<N>, option letters, (recommended)).

The rule costs 205 bytes per tier-2+ skill and 144 per tier-1 skill. A
further 25-byte cut to the bounded closer's exemption list leaves the net
for this wave at +17 bytes per tier-2+ skill and +158 per tier-1 skill.
review, qa and browse caps rise with measured notes; the ship goldens are
refreshed.

Co-authored-by: Xianying Tan <shrektan@126.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(redact-prepush): committed exact-span allowlist at .gstack-redact-allowlist

The pre-push hook never fed the engine's allowlist option, so a reviewed,
known-benign value (a public dev-compose URL, a documented sample key) warned
or blocked every push, and the only ways out were a bypass or reshaping code
until the detector stopped matching.

The hook now reads the repo-root .gstack-redact-allowlist from each pushed
commit (never the working tree), one exact matched span per line with #
comments. Only a finding whose matched text equals an entry is suppressed: a password alone, a
substring, or a different key still reports and still blocks, and engine.*
unscanned findings are untouched. Every push that carries entries prints the
entry count and each suppressed finding's rule and file line. Oversized files
(over 64 KiB) are ignored. gstack.redact.allowEmail keeps working as before.

Co-authored-by: Boyard Engels <boyard.engels@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(brain-sync): opt-in removal publishing with a bulk-removal valve

gstack-brain-sync never published a removal: a deleted synced file, or the
old directory after a project slug migration, stayed on the remote forever
and gbrain indexed both copies.

New setting artifacts_sync_removals (off by default). When on, each drain
commits and pushes, receipted like every push, the removal of tracked files
that are gone from disk and match the sync allowlist. Transcript pages are
never removed here, and a decision log goes only together with its active
snapshot. One valve: a drain that would remove more than 20 files at once
publishes none and says so. `gstack-brain-sync --publish-removals` lists the
pending removals and `--publish-removals --yes` publishes them on demand,
whatever the setting.

Co-authored-by: Jey2311 <5177215+Jey2311@users.noreply.github.com>

* feat(windows): setup.ps1 checks prerequisites and runs ./setup in Git Bash

A PowerShell entry point for Windows users. It checks that Git for Windows,
Bun and Node.js are on PATH (printing the winget command for each missing one
and changing nothing), finds Git Bash from git.exe rather than a PATH bash
that may be WSL's, and runs ./setup from the checkout with every argument
passed as its own argv entry to a constant bash script. No argument is
interpolated into shell source. ./setup itself is unchanged.

Co-authored-by: Jim McKeeth <jim@mckeeth.org>

* v1.91.63.0: CHANGELOG, version

* fix(ci): link bunx after the SHA-verified Bun install in GitLab CI and the Ubicloud setup

* fix(redact-prepush): strip every parent column from combined-diff lines, so a merged release bump is exempt like any other

* fix(hook-check): treat a Windows drive path as absolute (cygpath when present), so --list finds the skill hooks from a native caller

* fix(windows): owned-read requires the read-only attribute on Windows; setup.ps1 passes arguments NUL-separated through a private file; Windows-only tests expect the owned-read approval

* perf(hook-check): bundle every hook's TypeScript entries in one bun process instead of two bun starts per entry (seconds each on Windows)

* perf(hook-check): read all skill frontmatter in one awk and scan conflict markers with one grep per hook (half the process starts)

---------

Co-authored-by: thearbajonart <334330110+thearbajonart@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: DanCanadian <211164803+DanCanadian@users.noreply.github.com>
Co-authored-by: Nader Helmy <31414549+creatornader@users.noreply.github.com>
Co-authored-by: Nader Helmy <creator.nader@gmail.com>
Co-authored-by: Zee Zheng <228040220+spacegeologist@users.noreply.github.com>
Co-authored-by: Jack Thomas <17882510+loulanyue@users.noreply.github.com>
Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: techcenter68 <75930234+techcenter68@users.noreply.github.com>
Co-authored-by: Mattias Petersson <mattias.petersson@gmail.com>
Co-authored-by: Arno V <38716+aversini@users.noreply.github.com>
Co-authored-by: 0xDevNinja <manmit0x@gmail.com>
Co-authored-by: Benjamin D. Smith <7134745+BenjaminDSmithy@users.noreply.github.com>
Co-authored-by: Brian Majewski <1693720+bmajewski@users.noreply.github.com>
Co-authored-by: neallee2012 <1499497+neallee2012@users.noreply.github.com>
Co-authored-by: Max Hogan <max@maxhogan.dev>
Co-authored-by: maxpetrusenkoagent <280464918+maxpetrusenkoagent@users.noreply.github.com>
Co-authored-by: katlun-lgtm <264247399+katlun-lgtm@users.noreply.github.com>
Co-authored-by: mikehasa <91774998+mikehasa@users.noreply.github.com>
Co-authored-by: Briac <137724549+briacSck@users.noreply.github.com>
Co-authored-by: JackJin <1037461232@qq.com>
Co-authored-by: EmilianoU26 <314541067+EmilianoU26@users.noreply.github.com>
Co-authored-by: frosimanuel <frosimanuel@gmail.com>
Co-authored-by: Tom Lam <tomlam0811@berkeley.edu>
Co-authored-by: Yotam <yotsurfing@gmail.com>
Co-authored-by: Jinzhe <hwang.phy@gmail.com>
Co-authored-by: Xianying Tan <shrektan@126.com>
Co-authored-by: Boyard Engels <boyard.engels@gmail.com>
Co-authored-by: Jey2311 <5177215+Jey2311@users.noreply.github.com>
Co-authored-by: Jim McKeeth <jim@mckeeth.org>
2026-10-08 17:40:06 +00:00
+44 2db0b3adc8 v1.91.19.0 fix: severe fix wave (truthful gates, no lost memory, every host finds gstack) (#3025)
* docs: approved severe fix wave plan (Oct 2026)

* D0: keep a project's .env and bunfig.toml out of compiled binaries and daemon children

Compiled binaries are built with --no-compile-autoload-dotenv and
--no-compile-autoload-bunfig; the browse server and terminal agent are
spawned with --no-env-file and an empty config, so a repo's .env cannot set
GSTACK_CHROMIUM_NO_SANDBOX and a bunfig preload cannot run code in them.
Verified on Bun 1.4.0.

* A9: --no-write is a dry run and leaves ingest state untouched

--no-write stamped every prepared page ingested_at and saved state, so a
later real run skipped those pages forever. It now prepares and reports
pages without importing or changing the state file.

Tests that used GSTACK_MEMORY_INGEST_NO_WRITE / --no-write to seed state
move to an explicit seam (a fake gbrain that accepts every staged page),
because the --no-write contract deliberately changes.

* H #2885 #2827: vector geometry is not phone or card PII

pii.phone.e164 accepts "." as a group separator, so a single float
(viewBox="0 0 581.66796875 ...") and a run of path coordinates
("37.6188 101.694") read as phone numbers. pii.cc's \b stops at a decimal
point, so the long fraction of an .excalidraw float is a Luhn candidate.
Excalidraw's seed/versionNonce/updated integers read as bare phone numbers.
A rendered /diagram triplet raised 18 (svg) and 97 (excalidraw) MEDIUM
findings on main; it now raises none.

- looksLikeDecimalCoordinates (ported from #2885, extended to a single
  decimal and refusing a leading "+", the E.164 marker)
- insideDecimalNumber: a cc span glued to "<digit>." or ".<digit>"
- isNumericMetadataValue: an unquoted JSON value under a seed, nonce or
  timestamp key; the same digits under "phone" still report

Controls: E.164 in prose, dotted and spaced phones, "+"-prefixed runs,
"phone"/"contact" JSON numbers, Luhn-valid cards in prose and at sentence end.

Co-authored-by: Lewis <lewispeel@hotmail.com>

* D1: pin the terminal-agent /ws Origin to the gstack extension (#1324)

GSTACK_EXTENSION_ID moves to browse/src/extension-id.ts, shared by
/extension-token and the terminal agent's /ws gate. /ws now returns 403 for
every origin except the pinned extension. Forks override the ID with
gstack-config set browse_extension_id <id>, honored by both routes; the
BROWSE_EXTENSION_ID env var is no longer read. Test sites that used fake
extension IDs now use the pinned origin.

* H #2899: env.kv skips a function call assigned to a credential name

`session = _FlakySession(...)`, `token = make_token(user, ...)` and
`password = getpass.getpass()` raised env.kv MEDIUM, which blocks the
fail-closed decision log. The exemption needs the line's evidence, not the
span's shape: the value is unquoted, is an identifier or dotted path
followed by "(", and the call closes on the line or opens a multi-line
argument list. Quoted values, unclosed "(" inside an unquoted secret
(API_SECRET=Abc123xyz(9Qm) and calls carrying a high-entropy literal
argument still report. The literal check is shared with the #2912 env-read
exemption.

Co-authored-by: Yugz <yannduzelier@gmail.com>

* H #2962: per-machine config filenames are not internal hostnames

`CLAUDE.local.md` and `settings.local.json` reported internal.hostname
MEDIUM (the pattern's \b stops at the dot before the extension), which fails
closed in the decision log. A span immediately followed by a known config
extension (.md .json .jsonc .yaml .yml .toml .ini .conf .txt) and a word
boundary is a filename. Controls: `ssh build.local`, `printer.local.` at a
sentence end, `printer.local/md`, `printer.local.mdx`, a host with a port,
`api.corp`, `build-7.internal`. The `.env.local` vectors (#2740) pass on
main already via isDotenvFilename and are pinned here as regressions.

Co-authored-by: Florian <florian.cleyetmerle@gmail.com>

* H #2784: four-part versions need a version declaration, not a shape

`"version": "1.128.1.0"`, `<Version>`, `AssemblyVersion("1.0.0.0")` and
Keep a Changelog headings raised pii.ip_public MEDIUM on every release,
because a four-part version is byte-for-byte a dotted quad. Exempt only when
the same line declares it a version immediately before the value (a version
key or word, an XML tag, an attribute call, or a `## [x]` heading). The same
digits after host:/server =/"upstream", bare on a line, or after prose that
merely mentions a version still report. Unlike #2784, this does not exempt a
value because the same string is declared a version elsewhere in the text:
that let a real address sharing a diff with a matching version slip. The
pre-push hook's VERSION-file rule (#2856) is unchanged.

Co-authored-by: cdecook23 <decook23@gmail.com>

* H #2913: postgres:postgres is exempt only on a loopback host

The postgres image's default pair in compose files and CI DATABASE_URLs
blocked pushes as db.url_with_password HIGH. It is now exempt only for
postgres/postgresql URLs with exactly postgres:postgres on localhost or
127.0.0.1 (optional port). A bare single-label host (`db`, `postgres`), any
other host, a host that merely starts with localhost (`localhost.evil...`,
`localhost@prod-db`), any other user or password, and other schemes still
block. #2913 exempted the pair on any host; this keeps the plan's narrower
rule.

Co-authored-by: mb1810 <michael.berger1810@gmail.com>

* G1: ship without a version when no version source is configured

Resolve the version source to four outcomes (valid, absent, ambiguous,
broken) in lib/version-source.ts and read that one signal in
gstack-version-bump (new NO_VERSION state; write refuses to invent a
VERSION), gstack-next-version (no_version, no slot), /ship's bump,
CHANGELOG and title steps, /land-and-deploy's drift check and
/document-release. A configured version file that is missing, empty,
unreadable or malformed stops with its path; 0.0.0.0 is never
substituted. docs/skills.md documents .gstack/version-path.

Migrates two tests that pinned the old fabricated-0.0.0.0 behavior.

Co-authored-by: Koda <koda@edgeviewfinance.com.au>
Co-authored-by: Merit Blake <merit@newsprint.com>

* H #2949: eval credential screen checks every name segment

The hermetic eval env admits GITHUB_* and EVALS_* metadata but rejects
credential-shaped names, judged only by the last underscore segment. A
trailing qualifier carried a credential through: GITHUB_APP_PRIVATE_KEY_BASE64,
GITHUB_TOKEN_1, EVALS_API_KEY_FALLBACK reached eval children. The screen
now rejects a name when any segment is a credential word; segments, not
substrings, so GITHUB_PATH / GITHUB_TOKENIZER / GITHUB_KEYRING stay
metadata, and explicit runner admissions still win.

Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>

* H #2915: cover chunked scanning of large multi-file push diffs

Test-only coverage for behavior already on main: a clean two-file pushed
diff over the engine's 1 MiB cap passes without engine.input_too_large, and
a credential at the end of the second file (a later scan chunk) still
blocks the push.

Co-authored-by: pneumorea-ai <pneumorea@gmail.com>

* A3: keep a project's DATABASE_URL away from a PGLite brain

With a PGLite config, buildGbrainEnv returned the caller env unchanged, so
a project's .env DATABASE_URL (autoloaded by Bun) reached gbrain: a false
broken-db at best, gbrain talking to the app's own Postgres at worst.
Inherited DATABASE_URL and GBRAIN_DATABASE_URL are now blanked for PGLite
configs (GSTACK_RESPECT_ENV_DATABASE_URL=1 still keeps them), and every
gbrain child runs from gbrain's config dir instead of the user's project,
so it cannot reload the project's .env / .env.local.

The old test that pinned pass-through for an engine:pglite config is
migrated: pass-through now applies only to configs that name neither a
database_url nor PGLite.

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>

* C9: guard destructive gstack-upgrade and spec fences against unset or wrong paths

Fresh-shell hosts (and Claude Code, which keeps cwd but not shell variables)
ran cd "$INSTALL_DIR" with an empty variable, so git checkout/stash/reset
targeted the user's project. Step 2 now prints an absolute path; every
destructive fence starts with cd -- "${VAR:?...}" || exit 1 and checks it is
gstack's own checkout before any git command. /spec's worktree and spawn
fences get the same guard.

* F4: keep every paid design variant across regenerate rounds

$D variants now reserves a collision-safe round-<N>/ directory per run
(atomic mkdir) and writes a manifest.json of what each round saved. The
paid image request and the local write retry separately: a write failure
retries the write (temp + rename) and, if the path stays unwritable,
keeps the bytes in the temp dir instead of buying the image again.
Responsive variants land in their round directory too.

Readers updated: design-shotgun reserves a round dir and a private temp
dir for its parallel agents and records approved_path/round in
approved.json; design-html resolves approved_path (legacy letters still
work) and finds round-dir variants; design-consultation checks the
printed paths; the gallery shows round-dir variants. Variants that were
already overwritten before this change cannot be recovered.

Migrates the consultation contract test that pinned the flat path.

Co-authored-by: Matt Van Horn <mvanhorn@gmail.com>

* A2: stage dot-leading slugs gbrain can collect; network errors are db-unreachable

A page slug segment starting with a dot (a .claude project) staged a file
gbrain's import walker prunes, so the batch was refused on every run.
Leading dots become "dot-" per segment, and slugs recorded in state under
the old name map through the same function so no page is duplicated.

A DNS or connection failure (ENOTFOUND, EAI_AGAIN, ECONNREFUSED, ...) no
longer classifies as broken-db, which routed users to move a working
config aside. The new db-unreachable status carries the reason
"database host unreachable (<code> <host>); your gbrain config is
unchanged. Fix: check network or VPN, then re-run /sync-gbrain.", skips
local sync stages with that reason, and counts as usable for
gstack-gbrain-detect --is-ok like timeout.

Co-authored-by: Infiniteyieldai <infiniteyield.ai@gmail.com>

* D2: one address classifier for navigation literals, DNS answers, redirects and page-driven navigations (#2811)

classifyAddress() in url-validation.ts blocks 169.254.0.0/16 (metadata and
container credential endpoints), 100.100.100.200, fe80::/10 and fc00::/7,
including IPv4-mapped, IPv4-compatible and NAT64 spellings and decimal,
octal and hex IPv4. validateNavigationUrl, the A/AAAA answer check and
session-persist's cookie-domain guard all use it. BrowserManager now checks
every navigation request (redirect hops, link clicks, scripted navigations):
a blocked target resets the tab to about:blank and the write command fails
with the reason. Connection-time DNS rebinding remains out of scope.

Co-authored-by: Gabriel Rondon <grondon@gmail.com>

* C4: external hosts point at carved sections relative to the installed skill

Publishes the pointer form setup matches: on every non-Claude host a carved
section is `sections/<file>` relative to the installed gstack-<skill> SKILL.md
directory (QA's existing form), never $GSTACK_ROOT/<skill>/sections/, which
does not exist in external runtime roots. No render changes yet: only QA is
carved on external hosts today.

* INV-1: gate outcome contract for outside reviews

classifyOutsideReview() returns separate execution, findings and verdict
fields from text, stderr and exit status; reason codes, anchors and fix
actions live in lib/gate-outcomes.ts. validateOutsideReview() stays as the
text-only wrapper and the two-argument CLI keeps exit 0/1/2 for importers
and installed skills that predate a re-render. The --verdict form (implied
by --stderr/--exit) prints VERDICT/FINDINGS/REASON and exits 0/3/4/1; the
generated outside-voice caller moves to it and branches on each status
(unverified prints OUTSIDE_STATUS: unverified, never completed).

Adds docs/troubleshooting.md with one anchored section per reason code, the
anchor test, and the gate-tail lint (query scripts may not swallow failures,
gate calls may not end in || true, other best-effort tails say why).

* G1: quote the base placeholder in document-release title sync

The title-sync block is checked with bash -n; an unquoted <base> parses
as a redirection. Migrates the test that pinned the old root VERSION
read to the shared classifier call.

* G3: give every logging-bin mktemp a ${TMPDIR:-/tmp} template

Bare mktemp, mktemp -t and literal /tmp templates in the learnings,
question and preference logs (plus jsonl-merge, distill, the two
dashboards, /ship's PR body and /document-release's run dir) now use
"${TMPDIR:-/tmp}/<name>.XXXXXX", the form the INV-3 lint accepts. When
the temp file cannot be created, the learnings, question and preference
logs now say the entry was not recorded and how to fix TMPDIR instead of
exiting with only mktemp's own error. Three tests that pinned the bare
PR_BODY_FILE=$(mktemp) and one that pinned the /tmp run dir migrate.

GNU and BSD mktemp already honor TMPDIR without a template, so the
macOS-sandbox drop in #2952 is unverified here; the template makes the
temp location explicit on every mktemp.

Co-authored-by: Davon Johnson <d.johnson@dayofshow.net>

* B1b: structured outside-review gate fails on P0 as well as P1

validateOutsideReview() derived its structured pass/fail from a [P1] match,
so a review reporting only [P0] findings passed. The wrapper now reads the
classifier's findings verdict (P0 or P1), and the /review and /ship
structured-gate prose requests [P0]-[P3] tags and fails on P0/P1; untagged
output (OUTSIDE_STATUS: unverified) is missing coverage. The ship clarity
pin moves from 'structured P1' to 'structured P0/P1' with the behavior.

* E1: freeze normalizes boundary and target the same way on Windows; hooks spawn through cmd.exe (#2876, #2354)

gstack_hook_normalize_path (careful/bin/hook-extract.sh) maps C:\..., c:/...,
UNC \\server\share (leading // kept) and /cygdrive/c to one form; check-freeze
runs both the saved boundary and file_path through it before the absolute
test, after symlink resolution, and compares case-insensitively on Windows
bash. freeze, guard and careful register hooks as
bash -c "exec bash \"$HOME/...\"" so cmd.exe, which never expands $HOME,
no longer turns the hook into a silent exit 127.

Co-authored-by: Kevin Grasso <kevin@biblingo.com>
Co-authored-by: Xuan-Ce Wang <261591073+xwang4-svg@users.noreply.github.com>

* G4: keep CLI-to-daemon loopback calls off HTTP_PROXY (#494)

The CLI appends 127.0.0.1, localhost and ::1 to the user's NO_PROXY (and
no_proxy), keeping existing entries, before its first fetch. Verified on
Bun 1.4.0: fetch honors NO_PROXY when it is set before the first request
and ignores later changes, so it is set at the top of main().

* F1: bind the plan audit to this branch's plan, never the newest file

Plan File Discovery now prefers an explicit binding (the plan-mode file
or /autoplan ACTIVE_PLAN from the conversation, then a "Plan: <path>"
line in the PR body), then lists candidates: repo-committed
docs/designs/ files this branch changed or that name it, and personal
plan files that name the branch. Candidates are offered, never picked
silently. The repo-name match and the newest-file-of-the-day fallback,
which audited PRs against unrelated plans, are gone. With nothing bound
or chosen it prints the exact line 'Plan completion audit: not run (...).
Fix: add "Plan: <path>" to the PR body, or run /autoplan.'

/ship's parent now binds the plan before dispatching the audit
subagent ({{PLAN_COMPLETION_GATE_SHIP:discovery}}), so the child never
searches. Migrates three tests that pinned the old markers and notice.

Co-authored-by: loulanyue <260355617@qq.com>

* G5: browse type --selector <sel> <text> (#2936)

--selector focuses the element (sel or @ref) and types into it. The bare
form still types its text literally into the focused element; text that
starts with -- goes after a -- separator. When the first word looks like a
CSS selector, the result adds a hint to use --selector without echoing the
typed text, and never refuses based on page contents. Help and command
usage updated.

Co-authored-by: Thomas Reuser <thomasreuser@gmail.com>

* F2: design-doc discovery reads docs/designs, never a root DESIGN.md

A root DESIGN.md is the design system /design-consultation writes, so
preferring it unconditionally reviewed plans against a token file while
printing "Design doc found" and never looked at docs/designs/ feature
docs. Discovery now considers only docs/designs/*.md as the repo-local
design doc (still freshness-compared with the private ~/.gstack copy).
The CEO finding fixture seeds its supplied design as a docs/designs doc
and its test follows.

Co-authored-by: Bit0ps <alex.dreamscaper@gmail.com>

* C4: rename the external carve test (typecheck:test union-order artifact)

tsc prints a union's members in type-creation order; a test file sorting
before ceo-mode-option.test.ts reordered one baseline diagnostic's text.

* C1: every env-var host fence resolves its own gstack root

Codex and the other env-var hosts run each block in a fresh shell, so
GSTACK_ROOT/BIN, B and D set by the preamble were empty in later blocks
(/bin/gstack-slug, B=/browse). scripts/resolvers/runtime-root.ts now owns
root resolution: a valid exported GSTACK_ROOT (bin/ and lib/), else the
per-install literal root (no git call), else repo-local, else the global root
(CODEX_HOME on Codex); no root prints the setup fix line and exits. One
post-render pass inserts it (<=400 bytes) into every fence that uses these
variables without assigning them; B/D come from the same helper as BROWSE
and DESIGN SETUP. Claude output is byte-identical.

Migrated pins of the old preamble lines (gen-skill-docs.test.ts) and a
fixture whose exported GSTACK_ROOT lacked lib/ (skill-positional-literals).

* E8: headless browse honors GSTACK_CHROMIUM_PATH; extensions load in new headless mode (#2771, #1968, #2281)

The headless launch passes GSTACK_CHROMIUM_PATH as executablePath, scopes
the XProtect self-heal out for that bundle, and names the path in a launch
failure with the fix. BROWSE_EXTENSIONS_DIR now launches a persistent
context under the project state dir with --headless=new, so the extension
runs and no window appears (the off-screen window was visible on macOS and
the extension never ran in an isolated newContext). The GPU source guard
test moves from the removed useHeadless flag to launch() vs launchHeaded().

Co-authored-by: oli548 <312288720+oli548@users.noreply.github.com>
Co-authored-by: Slava <slava.istomin@tuta.io>
Co-authored-by: ryanlaiwy <129729476+ryanlaiwy@users.noreply.github.com>
Co-authored-by: 李翊楠 <yinanli1917@gmail.com>

* C2: link design/dist and make-pdf/dist into every host runtime root and the agents sidecar

Codex design and make-pdf skills fall back to $GSTACK_DESIGN and
$GSTACK_MAKE_PDF under the runtime root, but setup linked only browse/dist,
so both preflights reported NOT_AVAILABLE on a fresh install. One helper
links dist/ only (make-pdf/ ships a Claude SKILL.md) into the Codex,
Factory, OpenCode, Cursor, Copilot and Kiro roots, the .agents sidecar and
the Cursor sidecar.

Co-authored-by: Mammad M. <mammad@digit.az>

* C3: install router and nested SKILL.md files as real copies; back up hand edits; --status checks

Codex skips a symlinked SKILL.md file, so the gstack router linked into
~/.codex/skills/gstack was invisible. Every host runtime root (Codex,
Factory, OpenCode, Cursor, Copilot, Kiro) and Kiro's per-skill installs
now get real copies, rewritten on every run (the auto-update path runs
setup --refresh-registered, so it refreshes them too). setup records each
copy's hash in <state root>/skill-copies.tsv; a copy that no longer matches
is saved to a uniquely named backup under <state root>/backups/skill-copies/
before _activate_runtime_root replaces the tree, and a failed backup keeps
the old tree. setup prints what it refreshed or removed.

./setup --status now prints, per registered install, whether the router is a
real file and whether every section pointer resolves, with the fix command.

Test migration: setup-codex-model pinned the Kiro router/upgrade links as
_link_or_copy; C3 deliberately changes them to copies.

* INV-3: install-level host conformance checks (no symlinked SKILL.md, dist present, section pointers resolve)

For codex, kiro, factory, opencode, cursor and copilot, the real-setup
conformance kit now fails when an installed SKILL.md is a symlink, when
design/dist or make-pdf/dist is missing from the runtime root, or when a
'Read `.../sections/<f>.md`' pointer in an installed SKILL.md does not
resolve (bare pointers in carved skills, relative-to-installed-skill
pointers, and $GSTACK_ROOT/~/absolute/../ pointers everywhere), and
./setup --status must report 'all resolve'. No new setup runs.

Expected red until the generator lane lands: design-html's install-offer
pointer and plan-eng-review's review-sections pointer name
$GSTACK_ROOT/<skill>/sections/... on every external host.

* B11: auto-update tracks pull, setup and migrations; announces only on success; retries with backoff

gstack-session-update wrote the 'just upgraded' marker after SETUP_FAILED or
SETUP_SKIPPED bun_missing, and re-ran setup only when HEAD moved, so a failed
install was announced as an upgrade and never retried. Stages are now
persisted in <state root>/session-update-pending; the marker is written only
when every stage succeeded; a pending setup or migrations stage is retried
even with HEAD unchanged after the update-check interval, then 6h, then 24h;
and each session start prints one line naming the stage, the reason and the
repair command while it is pending. Best-effort tails carry a same-line
reason.

* D1: gstack-config key browse_extension_id for forks and self-built extensions

The browse daemon trusts one Chrome extension ID on /extension-token and the
terminal-agent /ws. The override now lives only in gstack-config (never an
env var a project .env could set): empty by default (gstack's published
extension), a 32-letter a-p Chrome ID when set, "" clears it, and anything
else is rejected with the stored value kept. The browse lane reads it.

gstack-config-defaults: browse/src counts as a reader directory; the key is
listed prose-only until the browse lane's reader lands (the stale-entry check
then asks for the entry's removal).

* G2: key the sticky slug cache on the git common dir

A linked worktree used to resolve its slug fresh from the remote while
the main checkout kept its sticky pre-remote identity, so learnings,
checkpoints and timelines split into two buckets. gstack-slug and its
TS twin in lib/bin-context.ts now key the cache on the git common dir
(read from .git files, no git spawn, so cache hits stay git-free) and
read entries in one deterministic order: the repository entry, then the
main checkout's pre-upgrade entry, then this directory's own. Both the
repository and per-path entries are written.

When a worktree's own old entry named a different bucket that still
holds data, that slug is recorded in projects/<slug>/.former-slugs and
reported by --get FORMER_SLUGS. New --adopt-legacy --from <slug> merges
it: JSONL logs append missing lines with de-duplication, missing files
copy, differing files are listed as conflicts and never overwritten,
the source bucket is kept, and it prints migrated/pending/conflicting
counts; reruns are idempotent. --adopt-legacy --dismiss <slug> drops
the reminder.

Co-authored-by: loulanyue <260355617@qq.com>

* C5: OpenCode /gstack-* commands load the skill by its frontmatter name

OpenCode's skill tool knows gstack-review as `review` (its frontmatter
name), and OpenCode's builtin /review shadows that name as a command. The
managed /gstack-review command told the agent to load `gstack-review`,
a name the skill tool does not list. Every managed command now names the
frontmatter name. OpenCode's AskUserQuestion/ExitPlanMode mapping (#2759)
already shipped in v1.91.16.0.

Test migration: the #2629 commands test pinned the directory name.

Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com>

* E4: Windows node-server bundle keeps sharp and socks external; CLI resolves its server script lazily (#2260, #2439)

build-node-server.sh passes --external sharp and --external socks (Bun on
Windows refuses to inline them; on Linux they were silently inlined). The
CLI no longer resolves server.ts or server-node.mjs at module load:
resolveServerLaunch() runs when a server starts, so a Windows minimal
runtime with only server-node.mjs works and a missing script is reported
only when one is needed.

Co-authored-by: Spooks444 <193448029+Spooks444@users.noreply.github.com>
Co-authored-by: punksterlabs <77217235+punksterlabs@users.noreply.github.com>

* B1: Codex reviews whose sandbox could not start are unavailable, not a pass

In containers without unprivileged user namespaces every Codex command
fails, yet the review exits 0 and concludes 'no findings' (captured in
test/fixtures/codex-sandbox/). The validator now takes execution evidence:
'codex exec --json' command events and the 'codex review' stderr transcript
(a command that succeeded proves execution); sandbox failure lines on
stderr or in failed command output return unavailable(sandbox_unavailable);
exact 'could not run commands / read the diff' phrases are the last
fallback. Review text that merely discusses bwrap or namespaces still passes.

A free 'codex sandbox ... true' preflight (Linux, 10 s) runs before the paid
model probe and every automatic outside review; CODEX_MODE gains
sandbox_unavailable, and an inconclusive model probe reports unverified
instead of ready (exit code kept for stale callers). Every Codex site
(outside-voice resolver, /codex review, challenge, consult) takes its sandbox
from _GSTACK_CODEX_SANDBOX and pipes its result through the validator; a
static test parses rendered command positions for both. GSTACK_CODEX_NO_SANDBOX=1
(exactly 1, shell env only) switches to danger-full-access with a warning on
every use. Sandbox pins in existing tests move from the literal read-only to
the selected-sandbox variable; captured autoplan probes are judged against
the probe block they executed.

* C6: gstack-relink discovers skills structurally, so /browse survives a skill_prefix toggle

relink skipped a hardcoded list of support-directory names that included
browse, a real skill, so toggling skill_prefix removed /browse. A top-level
directory is now a skill exactly when it carries a SKILL.md.

Co-authored-by: Jonathan Zhang <topcoder1@Mac-Studio.local>

* C4: carve ship and plan-ceo-review on every host; enforce the 160 KB byte gate

The generated ship skill was 202-241 KB on all 10 non-Claude hosts (and
Factory plan-ceo-review 160,039 bytes), over the 160,000-byte ceiling,
because section carving ran only for Claude. Both skills now carve on every
host, pointing at sections relative to the installed skill directory (literal
$GSTACK_ROOT/<skill>/sections refs are rewritten the same way). The ceiling
is measured in UTF-8 bytes and fails generation in repo mode; under
--install-root it warns so setup never leaves a user without skills.

qa-lazy-sections.test.ts pinned ship as inline-on-external-hosts; migrated to
the carved policy and kept the inline check on review (Claude-only carve).

* B5: learnings and timeline reads fail loudly when bun is missing

gstack-learnings-search and gstack-timeline-read ended in
'2>/dev/null || exit 0', so a missing bun or a crashed embedded script
printed nothing and exited 0, identical to 'nothing recorded'. Both now
exit 127 with '<script>: bun not found on PATH, so ... could not be
read. Fix: install Bun (https://bun.sh), then re-run ./setup.' and let
an embedded-script failure exit non-zero with its stderr. A successful
run with no matches still exits 0 with empty output. The generated
callers that print 'LEARNINGS: unavailable (...)' belong to the
generator lane.

Co-authored-by: loulanyue <260355617@qq.com>

* B2: a retired-model 404 reports MODEL_UNUSABLE, not CODEX_MODE: ready

The model probe matched only 400 / 'is not supported', so a retired model's
404 ('does not exist or you do not have access to it') and a custom
provider's bare 404 fell through to the inconclusive branch and reported
ready. Both now classify as MODEL_UNUSABLE with a hint naming the two causes
(retired model, or a wrong base_url) and log codex_model_retired; a
'requires a newer version of Codex' 400 points at upgrading the CLI and logs
codex_cli_outdated. Ported from #2843 onto the current probe.

Co-authored-by: Marek Cermak <marek.cermak@strv.com>

* B6: rank learnings by matched query tokens and disclose truncation

A more specific query used to drop the exact match: the token-OR filter
recalled every entry sharing one token, the sort used confidence alone,
and the default --limit cut silently. Entries now rank by how many
distinct query tokens they match, then confidence, then recency, and a
query that matched more than fit says '-- N more matched, raise --limit
to see them'. The hit count lives in a local map so a field stored on
disk can never become a sort key, and the type grouping uses a
null-prototype map so a row typed 'constructor' or '__proto__' cannot
crash the run. Ranking code ported from #2796; regression vectors
adapted from #2799 (its stemmer is not needed).

Co-authored-by: loulanyue <260355617@qq.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>

* C7: required team mode also blocks Copilot CLI's lowercase skill tool

Copilot CLI applies Claude-format matchers by tool name; its skill tool is
`skill`, which no Claude tool name maps to, so the 'Skill' matcher never
fired and Copilot ran protected skills without gstack. The hook entry now
matches 'Skill|skill', an entry an older team-init wrote is upgraded in
place, and the deny is emitted in both decision formats (Copilot reads the
top-level permissionDecision, Claude Code reads hookSpecificOutput) with
exit 0 so both parse it.

Test migration: the settings test pinned matcher 'Skill'.

Co-authored-by: neallee <neallee@gmail.com>

* B3: Codex auth probe accepts custom providers' env_key credentials

The auth probe only knew CODEX_API_KEY, OPENAI_API_KEY and auth.json, so a
working self-hosted or OpenAI-compatible provider configured with
[model_providers.<id>] env_key = "X" reported AUTH_FAILED and /codex
stopped. The probe now reads env_key from model_providers tables only
(comments ignored, the name must be a shell identifier before it is
dereferenced) and accepts a set variable; when none is set it names the
missing key on stderr. Ported from #2192 with the table scoping added.

Co-authored-by: hardy <932821449@qq.com>

* E8: keep the extensions profile under the state dir at a path lock cleanup accepts

cleanSingletonLocks only cleans directories named chromium-profile, so the
BROWSE_EXTENSIONS_DIR profile moves to <state>/extensions/chromium-profile.

* B10: plan-tune never reports CALIBRATED with zero signal events

Stats printed CALIBRATED: true from logged-answer counts alone, while
every answer could miss SIGNAL_MAP and leave each dimension at its 0.5
seed (#2623). --derive now records inferred.signal_events, the answers
that actually moved a dimension; calibration requires at least one, and
the summary prints 'not calibrated: no recorded signals' verbatim when
there are none (or says the count is unknown for a profile derived
before this change). The psychographic redesign stays out of scope.

* A1: verify gbrain landing before stamping transcripts, plus --reconcile

gbrain import exits 0 while skipping pages (returned per-file failures,
malformed filenames, managed-import Pending receipts), and its failure
ledger exists only for git import dirs, so a skipped page was stamped
ingested and never retried (field reports: 37 and 69 lost entries).

Write, verify, then stamp (lib/memory-ingest-landing.ts, a new ratcheted
module):
- State schema 2 with a transition table (staged -> imported_unverified ->
  ingested, plus refused, quarantined, unrecoverable). Schema 1 files
  migrate in place: entries become ingested in the legacy "default"
  source and a reconcile pass is marked pending.
- Named failures come from --json `failures`, stderr "Skipped <rel>:"
  lines and the ledger; a failure gbrain does not attribute refuses the
  whole batch. After 3 refused batches the batch is bisected and the page
  that poisons it is quarantined (printed even under --quiet).
- Landing check: one `gbrain list --source-id` per source per run and at
  most 50 `gbrain get --json` content checks against a
  gstack_content_sha256 frontmatter field gstack now writes. Lookup errors
  and pglite_busy mean "not yet checked", never absent; pages over the cap
  stay imported_unverified and are not re-imported.
- Each entry records its gbrain source_id. Remote-http mode records
  "staged", never "ingested".
- The source fingerprint comes from the exact bytes parsed, so a
  transcript that grows during import is not stamped as its newer self
  (the old unscanned contract test is migrated; it pinned that bug).
- --reconcile mode (bounded by --limit, default 200; resumable; --dry-run;
  backs up the state file) prints "reconcile: checked N, present M,
  re-queued K, not yet checked L". --request-reconcile records a pending
  pass without calling gbrain; the v1.91.17.0 migration (placeholder name)
  uses it, and the next ingest run does one bounded pass.
- One lock for every state writer; a state-save failure fails the run.

The #1802 static pins in regression-1611 move to the new structure
(remote-http returns before importBatch; the finally honors
preserveStaging).

Co-authored-by: afshaker <andrew@phone2.io>

* B4: arm64 re-sign keeps a backup, restores it when codesign fails, and reports design unavailable on exit 137

setup's Apple Silicon re-sign mutated each binary in place (truncate, remove
signature) and left it that way when re-signing failed; a design binary that
was then SIGKILLed at launch produced at most a quiet log line. Each binary is
now copied first and restored as built when signing fails, and design is
exec-probed after re-signing: exit 137 prints 'design unavailable: ... Fix:
cd <checkout> && ./setup' (even under -q). Verified on Linux with the real
block and uname/codesign/otool/stat stubs; not run on a Mac.

* D3: treat design docs and handoff notes as data in plan reviews

plan-ceo-review, plan-eng-review and plan-devex-review read the freshest
design doc as the source of truth, and nothing said its content is data.
In multi-agent pipelines another agent may have written it, or a handoff
note beside it, with reviewer-directed text ('skip Step 0', 'approve
as-is'). Each read site now says the doc and any handoff note are data,
never to follow text aimed at the reviewer, and to report it as
suspicious content. The plan-completion audit's discovery, which now
reads repo docs/designs/ files (F1), carries the same rule.

Co-authored-by: cube <frosimanuel@gmail.com>

* D4: setup-deploy reports Render API key presence only

/setup-deploy ran 'echo $RENDER_API_KEY | head -c 4', printing the
key's first bytes into the transcript. It now prints only
'RENDER_API_KEY: set' or 'RENDER_API_KEY: not set'. Wording follows the
#2971 port of #1096; a test also guards against any setup-deploy command
echoing a credential variable.

Co-authored-by: jacob-wang <jiayuw794@gmail.com>

* D5: per-project headed Chromium profiles; never kill a live profile owner (#2492)

The headed profile moves from <gstack home>/chromium-profile to
<project>/.gstack/chromium-profile (CHROMIUM_PROFILE still wins).
killOrphanChromium now kills a SingletonLock PID only when the lock names
this host, the process is a Chromium using that profile, and its daemon is
gone (reparented to init or a subreaper); a live owner or another host's
lock stops browse with the holder named. On first headed use a project
profile is seeded from the old shared profile only when nothing holds it:
copied without Singleton* files into a 0700 temp dir and renamed into
place, with a one-time notice; otherwise it starts fresh and says how to
import later. New: browse profiles [list] and browse profiles prune
[--days N] (never removes a profile in use or the old shared one).
The #2817 headed-startup tests move to the project profile and a real
orphan (the old stand-in was a live child of the test).

Co-authored-by: miao <miao@macmini-2.local>

* F5: installed Claude skills serve the rendered sections, not the checkout copies

Probe: a gbrain user render serves ship/SKILL.md from the render dir; its
STOP pointers name absolute render paths, but its section index names
`sections/<f>.md` relative to the installed skill directory, whose
sections/ was linked from the checkout. So the rendered 'Save Results to
Brain' blocks (plan reviews, office-hours, ship adversarial) were generated
and never read. setup's _link_skill_runtime_assets and gstack-relink (the
gbrain-refresh path) now serve a rendered counterpart per file, keep
checkout-only assets (manifest.json), return an asset to the checkout once
its render is gone, and never touch an entry that is not gstack's links.

Co-authored-by: Jonah Berg <jonah.berg.g@gmail.com>

* F1: ratchet /ship's parity union-size cap for this lane's content

G1's NO_VERSION arms, F1's parent-side plan binding and D3's data line
add 2,126 bytes to /ship's skeleton-plus-sections union, taking it from
1.4022x to 1.4135x of the v1.64.1.0 baseline (cap was 1.405). The cap
moves to 1.414 with the measurement recorded, as earlier growth did; no
other invariant changes.

* E5: feed every Codex prompt on stdin instead of argv

Prompts went to Codex as one argv string ('codex exec "$(cat file)"'), which
fails over 32 KB on Windows (and over 128 KB on Linux, E2BIG) and mangles
quotes. Every codex exec site (outside-voice resolver, /codex review custom
instructions, challenge, consult new and resume) now runs 'codex exec -'
with the prepared prompt file on stdin; challenge and consult write the
prompt with a quoted heredoc so it is never shell-parsed. The bash-native
timeout watchdog (stock macOS) started the command in the background, which
gives it /dev/null as stdin; it now passes the caller's stdin through.

The 'byte for byte' dispatch test drops its old argv trailing-LF exception,
the consultation and autoplan fixtures follow the stdin form, and the static
site test requires stdin at every rendered codex exec.

* G6: prettyscreenshot --hide keeps a trailing image path as the output path (#1419)

--hide collected every following non-flag argument as a selector, so the
output path was hidden instead of written to. A trailing argument that looks
like a path or ends in .png/.jpg/.jpeg/.webp is now the output path.

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>

* INV-3: render-level host conformance and generated-bash lint on every host

One render per host checks every fence of every SKILL.md and section file:
bash -n with angle-bracket placeholders normalized; the env-var host prelude
and its B/D consumers run under env -i and set -u with stub binaries (no
empty value, no /bin/, /browse, /design or /gstack- path); and a quote-aware
lint (test/helpers/generated-bash-lint.ts) fails on a ~/ word inside double
quotes, mktemp without a TMPDIR-respecting template, and cd/rm -r/mv/git -C
on a variable the fence never assigned unless written ${VAR:?...} (cd must
also handle failure). Negative controls plant each failure.

Templated the bare mktemp calls in generator-owned resolvers and the upgrade
fence; migrated the two tests that pinned the bare form.

* G6: analytics averages quoted duration_s values (#1728)

gstack-skill-end writes duration_s as a quoted string, and the per-skill
average stripped everything from the leading quote, so every average
read 0s. The awk now drops a leading quote before extracting digits.

Co-authored-by: Ryan Alberts <ryan.a.alberts@gmail.com>

* G6: gstack-ios-qa-daemon --help prints usage instead of starting the daemon (#1932)

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>

* G1: tighten /ship's NO_VERSION wording to fit the union-size cap

Same behavior in fewer bytes: the NO_VERSION arm, the broken-source
exit, the unprefixed-title rule, the Step 20 null and the CHANGELOG
skip. The cap ratchet commit measured this wording.

* G6: rejected taste preferences gain confidence (#1777)

bumpPref computed every entry's confidence from approved_count, so a
rejected entry, which never gains approvals, stayed at 0: show ranked
rejections by insertion order and the taste-drift warning could not
fire from real use. Confidence now uses the entry's own bucket count.
Also salvaged by Garry's #2971.

Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>

* E2: Windows /cso build disables only C5105, and a failed MSVC compile is not reported as a missing toolchain

The Windows SDK's winbase.h raises C5105, which /W4 /WX promotes to error
C2220, so every MSVC build of the native /cso helper failed, and setup
mapped every probe failure to 'install Visual Studio 2022 Build Tools'.
Both compile lines add /wd5105 (only that diagnostic), and setup keeps the
probe's output: a probe that found MSVC but failed to compile reports
windows-msvc-compile with the compiler's first error. Not run on Windows:
the mapping test stubs powershell.exe; the ps1 assertion runs everywhere.

Co-authored-by: AlinValentin7 <AlinValentin7@users.noreply.github.com>

* E6: gbrain-refresh hands the detection file to Python on stdin

Under Git Bash, a native Windows Python first on PATH cannot open an MSYS
/c/... path, so gbrain-refresh always summarized the detection as
'local-status: unknown' and brain-aware blocks were suppressed. bash now
opens the file and Python reads stdin; the grep fallback without Python is
unchanged. Verified on Linux with a stub Python that refuses path opens; not
run on Windows.

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>

* G6: context-save computes session duration on Linux (#2704)

The duration step parsed 'ps -o lstart=' with BSD-only 'date -jf', so on
Linux session_duration_s was always unknown. It now falls back to GNU
'date -d', and never parses an empty start (GNU reads that as midnight).
Ported onto the current block instead of #2704's ~/.gstack/sessions
mtime, which bypassed the state root.

Co-authored-by: Evgenii Lopatin <e75533@gmail.com>

* Q2: one-time notice naming the Codex provider and account for outside reviews

Outside reviews are on by default and use whatever Codex login exists
(#965). The first automatic Codex review on a machine now prints one
non-blocking notice: the provider (config.toml model_provider, else openai),
where the credential comes from (CODEX_API_KEY, OPENAI_API_KEY, the ChatGPT
login or the key saved by codex login; never the value), that reviews stay
on, and how to turn them off (gstack-config set codex_reviews disabled). A
marker in the gstack state root keeps it to once per machine. Tests that run
the generated command now use private state roots. Also shortens the
no-sandbox provenance line.

* Q1: validate browser output paths the way the kernel resolves them (#2971 salvage)

validateOutputPath walks the destination one component at a time, resolving
each existing component before the next, so link/../x, a dangling symlink
and a dangling symlinked parent can no longer route a write outside the
safe directories, while a new multi-level directory inside them is allowed.
scrape validates its manifest and every downloaded file, not only --dir.
Ported from PR #2971 onto current code.

* G6: retro test census counts non-JS suites, skips generated output

The census recognized only JS/TS-style names, so pytest and minitest
(test_*.py|rb), XCTest (Tests/ dirs, FooTests.swift), Terraform
(*.tftest.hcl) and Bats (*.bats) suites counted as zero, and a committed
build directory counted as authored code (and its Test/ subdirs as test
LOC). is_test now knows those conventions and never treats prose as a
test, generated output is skipped in the numstat pass, TEST_FILES_CHANGED
uses is_test, and TEST_FILES_TOTAL uses the same filename conventions
minus generated paths. Merges #2812, #2809 and #2037's patterns (#2037's
retro template edits target a block the metrics bin replaced).

Co-authored-by: Jino <dgfrty00@gmail.com>
Co-authored-by: snig-17 <santosh.m.tiwari@gmail.com>
Co-authored-by: Sholto McNeilage <sholto@nodenarrative.com.au>

* Q3: setup stops auto-installing the AskUserQuestion preference hook in Conductor and removes one it added

The PreToolUse question-preference hook breaks Conductor's native
AskUserQuestion round-trip even when it only defers (#2207), and setup
auto-installed it on every Conductor workspace setup. Now a Conductor setup
without an explicit opt-in never installs it and removes an existing one
(the question-log and AUQ-error hooks stay), printing how to keep it
(gstack-config set plan_tune_hooks yes). An explicit opt-in still installs
it. gstack-settings-hook remove-source gains --event to scope the removal.

Test migration: setup-plan-tune-hooks-noninteractive pinned the retired
Conductor auto-opt-in.

Co-authored-by: Phil Hie <philhie@outlook.com>

* E5: watchdog stdin test feeds the wrapper through a redirect

Callers run '_gstack_codex_timeout_wrapper ... codex exec - < file'. Bash keeps
stdin for a backgrounded command inside a pipeline but not behind a
redirect, so the pipe form passed on the old watchdog; the redirect form
reproduces the lost prompt.

* F4: type the variant-round fetch stub like the existing stubs

The test-typecheck ratchet flagged the zero-argument stub cast to
typeof fetch; it now takes the fetch arguments like
variants-retry-after's stub. Test behavior unchanged.

* A4: import transcripts into per-repository, non-federated gbrain sources

`gbrain import` ran without --source-id, so transcripts from every project
landed in the shared (often federated) default source.

- Each transcript partition imports with --source-id into its originating
  repository's own source (gstack-transcripts-<repo>, built with
  constrainSourceId, now exported from lib/gbrain-sources.ts). The source
  is registered machine-local and --no-federated before its first import,
  and the registration is cached in state. Partitions import one after
  another (PGLite is a single writer); a failing source no longer blocks
  the others, except a timeout, whose checkpoint must survive.
- If gbrain cannot register the source, that partition stays local and
  unstamped, and the run says why (even under --quiet).
- --include-unattributed sends repo-less transcripts only to the
  machine-local gstack-transcripts-unattributed source, keeps them on the
  machine when the brain is remote (Postgres, Supabase or remote-HTTP),
  and never writes them to the publishable ~/.gstack/transcripts staging.
- A page's recorded source is sticky: pages stamped before this release
  ("default") keep importing there, so no source gains a duplicate.
- The staging dir name carries -src-<id> so a resumed import keeps its
  source.

Test fakes that recorded only the last import now record every import of
one run, and answer `gbrain sources`; remote-http scan tests use an
attributed transcript, since unattributed ones no longer stage there.

Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>

* F3: load the taste profile, and report a failed slug lookup

The taste-profile probe double-quoted ~/.claude/.../gstack-slug, so the tilde
never expanded and /design-consultation and /design-shotgun always printed
NO_TASTE_PROFILE. Use quoteSafePath(); stop hiding gstack-slug's stderr and
print TASTE_PROFILE_UNAVAILABLE with the fix when the slug cannot be resolved.

* B4: DESIGN_READY only when the design binary actually starts

DESIGN SETUP (and office-hours' visual exploration) printed DESIGN_READY for
any executable binary, so one SIGKILLed at launch by an invalid macOS code
signature still passed. Launch it under the portable 10-second deadline
(gtimeout, timeout, or perl alarm); otherwise print DESIGN_NOT_AVAILABLE with
the exit code, 'timed out after 10s', or on 137 the code-signature hint and
'Fix: cd <checkout> && ./setup'. A success is cached by inode and mtime under
the state root so Gatekeeper's first-launch scan is paid once.

The probe launches the binary with no arguments (usage, exit 0): the design
CLI has no --version command today.

* C2: name the runtime root every way a generated preflight may read it in the dist test

The generator lane's C1 resolves D from $GSTACK_ROOT instead of
$GSTACK_DESIGN, so the test now exports GSTACK_ROOT as well; it still fails
on main (no dist in the roots) and passes with and without the C1 change.

* A5: brain-sync stage fails loud when the artifacts source indexed zero pages

The stage reported "curated artifacts pushed" whenever gstack-brain-sync
exited 0, but a git push is not gbrain indexing. After a successful push
the stage now reads the maintained artifacts source's page count
(GSTACK_BRAIN_SOURCE_ID, else the source whose path is the artifacts
worktree or state root): zero pages is ERR with the fix command; a
missing source or an unavailable count says so instead of claiming
indexing.

Co-authored-by: Jimmy Zeisweiss <4261258+jzeisweiss@users.noreply.github.com>

* B1: state the effective read-only sandbox in /codex review prose

The codex parity invariant requires the skill to state sandbox_mode="read-only";
the command now takes it from _GSTACK_CODEX_SANDBOX, so the prose names the
effective default explicitly.

* A6: report deleted-worktree gbrain sources; prune only with proof, on request

A deleted Conductor worktree leaves its gstack-code-<hash> source behind
and gbrain's autopilot fails on it forever. Local absence is not proof a
source is dead (machines sharing a federated brain register their own
paths, and its pages may be the only copy of uncommitted work), so gstack
never removes one automatically.

- Each sync counts consecutive misses for gstack code sources whose path
  is missing, prints "path unavailable" once per source with the review
  command, and records which repository each synced source belongs to.
- New gstack-gbrain-sync --prune-gone-worktrees (with --dry-run, whose
  plan it always prints first) removes a source only when its path was
  missing on two consecutive syncs, its parent is readable, recomputing
  deriveCodeSourceId() for that path on this host yields its id, and the
  repository's git common dir still exists and no longer lists the
  worktree. It re-checks the path right before removing (through the
  #1734-guarded safeSourcesRemove) and logs each removal.

* D0: pass the Bun child flags after 'run' so wrappers that rewrite 'bun run' argv keep working

bun run --no-env-file --config=/dev/null <script> has the same effect
(verified on Bun 1.4.0) and keeps argv[1] === 'run', which the
publication-lock fixture's preload injection relies on. The sidebar-tabs
source guard now expects the flags in the terminal-agent spawn.

* E8: count the BROWSE_EXTENSIONS_DIR persistent launch in the signal-flags guard

The new launch site carries handleSIGINT/SIGTERM/SIGHUP false like the
other three, so the guard's expected count moves from 3 to 4.

* D2: give the memory-leak reproducer's fake request isNavigationRequest()

wirePageEvents now runs the navigation guard on every request event; the
fake request adapter supplies the method real Playwright requests have.

* A7: run only the resolve_symbol_edges dream phase, detected first

/sync-gbrain --dream and the --full auto-build ran gbrain's full dream
cycle (~35 minutes, LLM phases) when the call graph needs only the
resolve_symbol_edges phase. The stage now runs
gbrain dream [--source <id>] --phase resolve_symbol_edges when the
installed gbrain accepts that phase (detected by behavior: gbrain
validates --phase before printing --help). Otherwise it skips the forced
dream and says the full cycle costs about 35 minutes and how to run it,
instead of starting it.

Co-authored-by: liutiming <timingliu@outlook.com>

* D0: keep the dotenv isolation test runnable on the Windows lane

The compiled probe gets its .exe name on Windows, and the terminal-agent
case (owner identity comes from ps) is skipped there.

* B9: read cycle_freshness from a doctor run that includes it

/sync-gbrain Step 3.5 and the --full auto-dream decision read CYCLE from
gbrain doctor --json --fast, which skips the DB checks that carry
cycle_freshness, so the answer was always "unknown". Both now read
gbrain doctor --json --scope=brain (DB checks, no skill-file walk), parse
its JSON even when doctor exits 1 for a failing check, and match the
issue that names this source: "never completed" is never, "last cycled
Nh ago" is completed. When the installed gbrain does not report the
check, they say "unknown: installed gbrain does not expose
cycle_freshness" (lib readCycleStatus carries the reason to the --full
SKIP row).

The source-probe fixture that pinned the --fast call and a made-up
message format now answers --scope=brain with gbrain's real message.

* B4: compact the launch probe (cache keyed on inode, valid while newer than the binary)

Keeps design-consultation inside its parity size budget.

* C4: migrate the ship docs-dispatch test to the carved external ship

On Codex and Factory the documentation dispatch now lives in
sections/documentation.md behind a pointer relative to the installed skill,
like Claude; the test pinned the old inlined layout.

* B5: learnings callers report a failed search instead of reading as empty

Every generated gstack-learnings-search caller ended in 2>/dev/null || true,
so a missing bun or a failed search looked like 'nothing recorded'. Callers
now keep stdout, capture stderr and the exit status, and print
'LEARNINGS: unavailable (<first stderr line>)' on failure (resolver callers
in learnings.ts and review-army.ts, plus investigate, qa and ship's
adversarial section).

* F1: ship and review count unverified/unavailable outside reviews as missing coverage

INV-1 consumers in this lane's templates: /ship's readiness note, its
PR body Pre-Landing Review section and /review's completion rule now say
an outside review reporting unverified or unavailable is missing
coverage, never a pass. The PR body's Plan Completion section prints
F1's not-run line instead of the old 'No plan file detected.' The /ship
union-size cap moves to 1.415 for these lines (measured 1.4148).

* A8: rename the artifacts repo with OWNER/REPO, rewrite the remote only after it

The v1.27.0.0 migration passed a bare repo name to gh repo rename / gh
repo edit, which always fails (gh wants [HOST/]OWNER/REPO), then step 2
rewrote ~/.gstack-artifacts-remote.txt to the never-renamed repository.

- Qualify both names with the owner from the recorded remote URL (else
  the signed-in gh user) and surface gh's error.
- Step 2 moves the remote file but rewrites the URL only once the rename
  is confirmed; a later successful retry rewrites it then.
- A pending journal now counts as state to migrate, so the #1383 retry
  actually re-runs step 1 (it used to see no legacy file and declare the
  migration done).
- The v1.91.17.0 migration (placeholder name) repairs installs already
  hit: when the remote names gstack-artifacts-<user> that does not exist
  but gstack-brain-<user> does, it restores the old URL in the remote
  file and ~/.gstack's origin and says so. It only calls gh for such a
  remote and never fails the upgrade.

Co-authored-by: Brandon Pugsley <aibpaem@gmail.com>

* B8: read gbrain JSON with bun instead of jq in install and wireup

gstack-gbrain-install read package.json with jq but never checked for it,
so on a machine without jq a successful install failed with "cannot read
version". gstack-gbrain-source-wireup hard-failed without jq ("brew
install jq" is not actionable on git-bash), so the wireup was skipped on
every run. Both now read the JSON with bun, which they already require.

Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com>

* B4: shorter probe messages (design-consultation parity budget)

* B5: shorter capture variable names (parity budget)

* E7: find the Aside CLI in ~/.local/bin when it is not on PATH (#2902)

The installer puts aside in ~/.local/bin, which is not on the default macOS
login PATH, so the probe said NEEDS_ASIDE ('download Aside') for a healthy
install. The probe now falls back to ~/.local/bin/aside, drives the resolved
binary, and prints READY: <path> when it is off PATH so later blocks run that
path. Ported from PR #2965 onto the current _gs_d probe.

Co-authored-by: Som Samantray <som.samantray@gmail.com>

* A2: key the dot-slug catalog entry on the message users actually saw

* E1: cmd.exe-safe investigate freeze hook

On Windows Claude Code runs hook commands through cmd.exe, which cannot parse
the single-quoted bash -c argument, so /investigate's scope-lock hook never
ran. Use one double-quoted bash -c script that names the hook path directly
(no S= variable: POSIX sh would expand $S to empty and skip the hook).

* F4: design resolver reads per-round variant paths

$D variants now writes $_DESIGN_DIR/round-<N>/variant-*.png and prints JSON
with roundDir, manifest and paths. The mockup flow and comparison-board loops
build the board from those printed paths instead of hardcoded
variant-{A,B,C}.png, and approved.json records approved_path and round so a
round-1 choice made after round 2 still resolves.

* E4: generators tolerate Bun's spurious EEXIST on Windows (#2329)

Bun on Windows throws EEXIST from a recursive mkdir of an existing directory,
so gen:skill-docs and the llms.txt writer crashed once a second file landed
in a directory. Both now use lib/fs-utils' mkdirpSync (ported from PR #2329
onto the existing helper instead of a new scripts/fs-utils.ts).

Co-authored-by: Vaughan <vaughan@cairnsitguy.com.au>

* C7: keep the required-mode hook's exit-2 deny

Copilot CLI treats a preToolUse exit 2 as deny (its hooks reference), and
#2413 established exit 2 plus hookSpecificOutput as the shape Claude Code
enforces, so the deny output stays as on main; only the matcher and the
in-place upgrade of older 'Skill' entries change.

* C3: guard Kiro's router copies on a present source; give the QA pointer adapter the copy and dist helpers

A copy fails where a symlink dangled, so the Kiro router and upgrade copies
are written only when their render exists (as every other runtime root
already does). test/qa-lazy-sections' extracted Kiro installer now supplies
_link_runtime_dists (C2) and the SKILL.md copy helpers (C3).

* Q3: office-hours asks open-ended questions in a Q<N> prose form (#2719)

In Conductor, and whenever AskUserQuestion fails, prose is the only channel,
and the prose fallback defined only the D<N> decision brief. Phase 2A/2B
diagnostic questions have no option set, so the model dropped the phase.
Office-hours' preamble now defines the Q<N> open-question form (question,
why I'm asking, what a strong answer sounds like) and routes free-text
replies to it; the diagnostic sections name it. Scoped to office-hours so
every other skill's preamble is unchanged. Ported from PR #2729.

Co-authored-by: Bharadwaj Mittapelli <bharadwajmittapelli@gmail.com>

* A2: render db-unreachable like timeout

gbrain's new db-unreachable status is a transient network failure; a setup
run while offline must not strip brain-aware blocks from every skill.

* A2: treat db-unreachable like timeout in gbrain-refresh

The memory lane's new db-unreachable status (network or DNS failure, config
intact) is transient, so gbrain-refresh keeps rendering brain-aware blocks
for it exactly as it does for timeout instead of suppressing them.

* C8: a render given disabled skills drops them from the router (#3018)

The root router named every skill and ends 'When in doubt, invoke the skill',
so it routed to skills the user disabled. gen-skill-docs takes
--disabled-skills (runGeneration disabledSkills): the router drops their
routing rules, keeps the other side of an 'A or B' rule, and names them as
disabled. The router and gstack-upgrade cannot be disabled. With no disabled
skills the render is byte-identical.

* G2/G3/INV-1: skill-start former-bucket notice and visible learnings failure; TMPDIR mktemp in gbrain bins; best-effort reasons

* C9: Step 2 falls back to the install registry's source checkout (DX-7)

Codex users who installed from the README's ~/gstack clone got 'ERROR:
gstack not found' from /gstack-upgrade: Step 2 only looked under host skill
directories. It now falls back to the source checkout the install registry
recorded for the current host.

* B4: design --version prints the version, and the probe uses it

The design CLI treated --version as an unknown command (exit 1), so the
probe launched the binary with no arguments instead. Add --version (prints
the gstack version, exit 0, before auth or network) and probe with it, so
messages read '<path> --version exited <code>' as planned.

* C4: no external render points at a section file that is not installed

design-html's detector install offer and plan-eng-review's self-check pointed
at $GSTACK_ROOT/<skill>/sections/<file> on every external host, where neither
skill is carved and no section tree exists. design-html now inlines the offer
off Claude, and a skill's pointer to its own {{SECTION}}-inlined section
names the inlined text.

* C1: skillify finds the browse SDK under its own host's install root

The template hardcoded ~/.claude/skills/gstack for the canonical
browse-client.ts, so external hosts read another host's install or nothing.
A new INSTALLED_ROOT resolver (sibling of INSTALLED_BIN_DIR) names the host's
global root, or the per-install root when setup renders for one.

* C4: rewrite section refs on hosts whose root stays a literal ~/<globalRoot>

Kiro, OpenCode, Cursor and Copilot render ~/.claude/skills/gstack as their own
literal root, not $GSTACK_ROOT, so plan-eng-review's self-check pointer
escaped the rewrite (found by the setup lane's installed-pointer check).

* Integrator: outside-voice fences use the shared runtime prelude; TMPDIR mktemp and per-round variant paths in design templates

* Integrator: C8 router omits disabled skills on every render (relink renders the served Claude router in place); Windows-safe E4/E6 entries; investigate hooks under the cmd.exe check; browse_extension_id reader; order-insensitive test typecheck identities

tsc printed one unchanged ceo-mode-option diagnostic's object members in a
different order (content?/isError?) once lib/fs-utils joined the generator's
import graph; identities now sort object-type members, and old baselines
normalize on read.

* Q1 (#2971 salvage): bound iOS proxy responses

One wall-clock deadline covers headers and body (stalled and dribbling bodies
time out), the whole body is capped at 64 MiB (upstream_response_too_large),
every terminal path clears its timer, removes listeners and destroys the
request, and the daemon threads the deadline through both listeners. Ambiguous
mutations are still never replayed after a timeout or abort. Ported from
#2971's proxy-transport work plus the size bound.

* I1: grade the AUQ matrix's (recommended) marker on option labels, the field the auto-decide hook reads

The decision-brief format puts a Pros / cons block with 'A) ... (recommended)'
in the question text and the same marker on the option label. The matrix
counted both and failed all six skills in periodic run 37151477069 with
'expected exactly one (recommended) option, found 2'. The question-preference
hook reads labels only, so the gate now checks the native question: a
Recommendation line in the question text and exactly one label ending in
(recommended). Fixtures move to the native question shape and add the real
spec capture plus a marker-only-in-text negative control.

* Q1: type the proxy-transport test doubles (typecheck:test ratchet)

* INV-1: gate outcome rows and troubleshooting sections for every lane's new messages

* Release v1.91.19.0 (next free patch in the queue: #3022 holds 1.91.17.0, #3023 holds 1.91.18.0); migration renamed; engines.bun >=1.4.0 to match the CI pin (#2815)

* v1.91.19.0 docs and metadata: CHANGELOG, TODOS, README troubleshooting/upgrade/type --selector, BROWSER.md type and profiles, AGENTS.md conventions, CONTRIBUTING #2949, touchfiles for the wave's new lib/bin files

* I1: give hermetic Codex runs a temp dir beside CODEX_HOME so Codex can create its PATH aliases

Codex 0.160 refuses to create its helper aliases (codex-linux-sandbox among
them) when CODEX_HOME sits under its temp dir, and the runner put the
hermetic home under the shard TMPDIR. Periodic run 37151477069 shows the
warning and 'Unable to spawn codex-linux-sandbox ... No viable candidates
found in PATH' for every exec_command in codex-sol-scope-termination.

* I1: Codex evals fail a review whose commands never ran and stop reading tool errors as skill-load problems

validateCodexReview accepted codex-review-findings in periodic run 37151477069
although its only command failed in bwrap ('I couldn't run the review'); it
now requires execution evidence from classifyOutsideReview (B1's INV-1
classifier). codex-sol-scope-termination failed on 'apply_patch verification
failed: invalid hunk' because the skill-load check matched any 'invalid';
it now matches Codex 0.160's skill-loader diagnostics only.

* I1: run paid eval containers with seccomp unconfined so Codex's sandbox can start

Docker's default seccomp profile blocks the user namespaces bubblewrap needs,
so every Codex command in a paid eval failed with 'bwrap: No permissions to
create a new namespace' (codex-review-findings, run 37151477069). Reproduced
in the CI image locally: default profile fails, seccomp=unconfined runs the
command. The runner stays non-root.

* I1: shared-libs plan actor reads one exclusion grammar for scope lists

Periodic run 37151477069 refused 'Existing-caller migration and helper
hardening stay out of scope' as a hardening proposal: the actor had two
partial exclusion rules (unanchored is/are/remains outside, and an anchored
list that only stays unchanged), the third refusal at this boundary after
36633323521 and 36776104571. One anchored predicate now covers a bare list
of scope nouns (hyphenated compounds included) with is/are/stays/remains and
unchanged/untouched/excluded/out of scope/outside/not part; any other verb
in the clause is still read as a proposal. Adds the real capture and two
adversarial controls.

* Parity caps: measured v1.91.19.0 union/skeleton sizes for ship, review, design-consultation, office-hours and design-shotgun, with the reason beside each

* Regenerate generated output for v1.91.19.0 (gen:skill-docs --host all, goldens, llms.txt, context-budget fixture)

* I1: plan-eng-review writes the Test Plan Artifact even while test choices are pending

Periodic run 37151477069 (plan-eng-review-artifact): a non-interactive review
recorded every Test review choice as pending and never wrote the QA test
plan, because Step 5 said to write the artifact 'when these test and eval
choices are resolved' while the artifact section says to list unresolved
choices as pending. Both now say to write it after the decision gate, with
unanswered choices under Pending Decisions. The pinned sentence becomes a
meaning-level check.

* I3: plan-eng-review states when findings first enter the report and that the scope menu's recommendation needs no repo probe

Judge rationales for 'plan-eng-review/SKILL.md sections' (actionability 3.67
in periodic run 37151477069, clarity 3 in 36385945043 and 35567915613) cite
two concrete tensions: 'do not add findings before Scope Challenge C' versus
the ledger saves C itself performs, and 'Recommendation: A when a branch diff
exists' versus 'do not probe for session state'. The write policy now says
findings first enter through Scope Challenge C's ledger saves; the selector
says its Recommendation line advises the user and needs no repository
inspection. The pinned sentence becomes the new meaning-level regex.

* I3: plan-ceo-review defines the closing sequence's cleanup step

Two of three judge samples for 'plan-ceo-review/SKILL.md modes' in periodic
run 37151477069 (actionability at the 4.0 gate) flag 'perform permitted
cleanup' as never defined. The only cleanup the skill has is archiving old
CEO plans, which needs approval; step 5 now says so.

* I3: /qa-only's value-card rule names only its own card location

qa-only rendered the /qa line 'Put it in the 8e.5 record (/qa) or under each
proposed test (/qa-only)'; judge samples for 'qa-only/SKILL.md workflow'
(actionability at the 4.0 gate in run 37151477069, 3.67 in 36907895813)
flag the /qa-only-irrelevant 8e.5 record. The qa-mode bar now renders the
invoking skill's location; both skills get shorter.

* test-of-test ratchet: a repo script or setup read by path counts as product code

codex-invocation-sites (runs scripts/gen-skill-docs.ts), gstack-config-gbrain-refresh
(copies bin/<name> through a variable) and setup-cso-windows-probe (reads setup and
scripts/build-cso-windows.ps1) exercise product code the detector could not see.

* I1: DX floor actor strips the editor hint from the footer line only (from #2971)

The custom-field reply normalization removed 'ctrl+g to edit in <editor>'
from every line, so question text that resembled the menu footer could be
rewritten and still authenticate. Only the last non-empty line is the
footer. Ported from Garry's #2971 with its negative control.

* I4: attribute every census testcase; deselections never count as SKIPPED

The paid report counted every skipped JUnit testcase as SKIPPED and every
testcase whose name was not a registry id as unattributed. On the 2026-10-03
census (run 37151477069) that was SKIPPED 82 / unattributed 72 (periodic) and
SKIPPED 282 / unattributed 24 (gate), and every case-shard sibling also wrote a
skipped history record that reset its pass-rate series.

- attributeJUnitCase: a name is its registry id or CASE_TEST_NAMES label, else
  the only id its file registers, else the keyless file itself. Skipped
  other-tier cases, case-shard siblings, isolated or unselected cases and Bun's
  '(unnamed)' hook placeholders are zero-credit deselections with a reason and
  write no history record. Anything else is listed by name as unattributed.
- One result and one history record per case per shard; a genuinely skipped
  selected case counts as SKIPPED and is listed with its reason, also in
  report-summary.md and collector-outcomes.json. SKIPPED panels show their
  reason and skipped trials are no longer called assertions.
- CASE_TEST_NAMES labels for the three /cso cases.
- tierSkipReason: a computed registration whose lane-tier ids are all
  registered to other test files is excluded at plan time, so the four gate
  and two periodic files that ran with every test skipped no longer take a
  runner.

Census rerun: periodic SKIPPED 0 / unattributed 0, gate SKIPPED 0 /
unattributed 0; verdicts unchanged.

* I5: split the deploy file per case; guard PR-lane shard walls at 600 s

test/skill-e2e-deploy.test.ts ran 668 s as one gate shard in the 2026-10-03
census (six sequential cases, 51-201 s each). It is now case-sharded, so each
case is its own shard with its own (smaller) supervision budget; no timeout
or budget was raised.

- CASE_SHARDED_FILES gains the deploy file (names already equal ids).
- Gate duration seeds for the six case keys from the census JUnit walls, so
  packing stays at 12 gate-census slices (CI cap 230m -> 200m).
- shardDurationViolations + PR_LANE_SHARD_LIMIT_MS: a free test fails when a
  planned gate shard's recorded wall (committed seed, and the last gate census
  in test/fixtures/paid-report-census/gate-census-durations.json) exceeds 600 s.
- Tests that pinned deploy as one shard follow the split (cookie planner keys,
  reuse fixture key, release/PR floors).

* TODOS: /review fingerprint under Codex's read-only .git sandbox (found by the live Codex check)

* Free-suite repairs from the first integrated Ubicloud run

Product:
- Outside-voice fences carry runtimeRootPrelude(ctx) explicitly again, so the
  resolver output runs on its own (the post-render pass skips fences that
  already assign GSTACK_ROOT); outsideVoiceRuntime stays deleted.
- D5: an explicit CHROMIUM_PROFILE (gbrowser's per-workspace gbd) is never
  seeded from the shared profile or registered for `browse profiles prune`;
  this was also the free suite's write to the real ~/.gstack/browse-profiles.json.
- gstack-ios-qa-daemon: BASH_COMPAT=50 for its >512-byte --help heredoc.

Tests migrated to the wave's deliberate contract changes:
- B1 heading admits `unverified`; sandbox flag is ${_GSTACK_CODEX_SANDBOX:?};
  verdict-form output (VERDICT/FINDINGS/REASON); fake codex follows the
  preflight + stdin + -o form; Q2 notice marked shown in a private state root.
- C1: an exported root needs bin/ and lib/; a lone GSTACK_BIN is not honored.
- C4: plan-ceo-review is carved on every host (carrier, discovery and
  methodology fixtures read sections/ when the render has it).
- E7: READY prints the resolved Aside path.
- INV-3: the cookie-approval reconstruction reverses the bun-install mktemp template.
- F1's `Plan:` binding read gets a reasoned tracker-guard exemption.
- Browse stubs stop their server instead of scheduling process.exit; the
  headless launch check only counts browsers its own launch started.

* Regenerate generated output after the outside-voice prelude repair

* I1: revert the seccomp-only eval container option; it does not let Codex's sandbox start

Run 37158847998 on this branch and a stock Ubicloud VM both show that
seccomp=unconfined only moves bubblewrap's failure from 'No permissions to
create a new namespace' to 'Failed to make / slave: Permission denied'.
Codex's sandbox started only with the host sysctl
kernel.apparmor_restrict_unprivileged_userns=0 plus seccomp and AppArmor
unconfined on the container, which a container job cannot set from its
steps. The relaxation bought nothing, so it goes; the fix is filed as a
TODO for the integrator.

This reverts commit ec97187.

* I1: a Codex review eval needs a completed command, not just review-shaped text

Run 37158847998 recorded codex-review-findings as passed again: the agent
wrote 'I couldn't run the review because the execution sandbox failed
(bwrap: Failed to make / slave)', the failure never reached stderr or a
command event, and no phrase rule matched. validateCodexReview now also
requires at least one command_execution event that completed with exit 0.
Fixtures gain that event; the CI output is a new negative control.

* Evals lane follow-ups: close the skillify gate TODO (I2 green), file its open reds, CHANGELOG lines; refresh measured parity annotations

* Regenerate generated output after merging wave/evals

* D2: bound the navigation guard's tab reset (commit, 5 s)

Ubicloud run 3: a click to a link-local link reported the block, but the
reset's page.goto('about:blank') waited the full 30 s default navigation
timeout, so the command hung past the test deadline and the stale block
leaked into the next command. The reset now waits for commit only, with a
5 s bound.

* I1: run Codex paid slices on a host-run job where bubblewrap can start

Docker's default seccomp/AppArmor profiles and the Ubuntu 24.04 host's
kernel.apparmor_restrict_unprivileged_userns=1 keep Codex's bubblewrap
sandbox from starting in a job container, so every Codex command failed.
The planner now packs Codex shards into their own slices (plan.codexSlices,
validated against the entries); evals-periodic runs those slices on a new
host-run job that lifts the host restriction and runs the same CI image
with seccomp and AppArmor unconfined. Artifact names match eval-slices,
so the report reconciles one manifest. Gate plans have no Codex slices.

* I1: the /codex review eval passes only when Codex executed

Run 37158847998 recorded codex-review as passed while the sandbox
preflight said unavailable and no review ran: the case only checked the
session exit and an optional output file. It now requires the shared
validator's executed verdict (findings, clean or unverified) in the
codex command's own output and a written, non-fail-closed GATE.

* TODOS: characterize the free-suite timing reds from Ubicloud run 4

* I1: QA evidence names the probes to rerun after an input change, before the one-shot materialize

ship-exploratory-late-input returned inconclusive in gate census 37158847998:
the fixture locale changed after captures 001 and 002, the agent reran only
input 9, and materialize (which publishes once) recorded capture 001 as open.
Its next line then said to rerun what is open, which the published verdict no
longer allowed. Every capture now lists the commands that observed an older
input snapshot and have no rerun on current inputs (revalidate, plus the next
line), and materialize's next line says the verdict is final.

* I1: the eng batching counter reads records under "Section N — <review> review" headings

plan-eng-multi-finding-batching failed in 6 of 12 runs with reviewCount 0-1
(or a timeout with every ask counted as Step 0). In run 37158847998 the agent
asked 11 separate questions, each with a saved ledger record, but filed
records R2-R10 under '## Section 1 — Architecture review'. The counter strips
'Section 1' but not the dash separator, so it rejected every record outside
the Decision ledger and the floor never filled. Quoted, historical and
unrelated headings still do not count.

* I1: plan-eng-review-artifact names its report file without making it the only output

Runs 37151477069 and 37158847998 wrote the whole review, including the
GSTACK REVIEW REPORT, into review-output.md and never ran the Test Plan
Artifact block. #3017 made the artifact a hard assertion and dropped the
prompt's explicit artifact instruction, leaving 'Write your review to
review-output.md' as the one named destination. The prompt now names that
file as the selected report file and says every other output keeps the
destination the skill gives it, without naming the artifact.

* browse: close returns when Chromium exits; Xvfb cleanup does not wait on its own zombie

BrowserManager.close() detached every 'disconnected' listener before
browser.close(). That also removed Playwright's internal listener that
resolves close(), so every launched-mode close waited the full 5 s race
timeout (measured 5,000 ms on origin/main and on this branch; raw Playwright
closes in ~50 ms), and Playwright 1.62 has no Browser.process() for the
SIGKILL fallback. The daemon's SIGINT shutdown therefore took over 5 s, which
reddened the CLI lifecycle test under load (1 of 8 on main) and left browsers
alive at shard cleanup. gstack now detaches only its own listeners: close
takes ~50 ms.

cleanupXvfb treated a terminated Xvfb child as alive until our own blocked
event loop reaped it, so every close waited a full second then sent SIGKILL
to a zombie. A zombie now counts as exited; the display test counts only
running Xvfb processes.

* I1: office-hours-brain-writeback reads the long skill with Read and skips the preamble

4 of 18 trials ended error_max_turns at the unchanged 12-turn budget with a
gbrain search but no gbrain put. The failing trial in 37151477069 read the
1260-line rendered SKILL.md with cat, got truncated output, spent four more
turns re-reading it with sed, then ran the preamble's skill-start block. The
passing trials skipped the preamble. The prompt now says to read the skill
with the Read tool and skip the preamble, onboarding, telemetry and
contributor sections, as the plan-eng fixtures already do. Budget unchanged.

* CI: no credential-shaped URLs in added lines (quality scan); release floor measured on the merged tree

The quality job's credential scan of the PR diff flagged 7 db.url_with_password
lines: three docs lines now describe the login instead of spelling a URL, and
three tests build the URL from parts. paid-retry-supervision's periodic release
floor is the merged tree's measured 35,711 (main's value; the evals lane's
33,821 predates main's new paid files).

* qa-evidence: hold the stdout-pipe cases on a FIFO, not on a paused child stream

Ubicloud run 4 finished the blocked-forever case in 71 ms (it should take the
5 s write deadline): the 2 MB write was not held, because saturation depended
on how the runtime buffers a paused child pipe. On POSIX the child's stdout is
now a FIFO the test reads only when it chooses to, so the kernel holds the
write; Windows keeps the paused pipe.

* TODOS/CHANGELOG: the three run-4 timing reds are fixed at their causes (daemon close latency, FIFO-held pipe tests)

* I1: a QA helper call bun could not load is checked at the installed path, not failed outright

review-exploratory-small-cli failed gate census 37162480720 with 'command
outside declared caller observation interface': the actor dropped one
character from the shard directory in a gstack-qa-evidence capture path, bun
refused to load it ('Module not found', exit 1), and the next call ran the
same capture correctly. A failed call whose only output is bun's refusal for
a gstack-qa-evidence/deadline path is now held to the interface as the same
command at the installed helper path, so a compound or undeclared command
still fails.

* I1: plan-eng-review's final self-check names its three outputs

The fixture-prompt repair did not hold: in 37162480720 plan-eng-review-artifact
again wrote one review-output.md, ran the skeleton's read-back gate on it and
stopped without the QA Test Plan artifact (no gstack-paths/gstack-slug block).
The transcript shows the agent's last check before finishing is the skeleton's
Section self-check, which said only 'and outputs'. It now names the saved
report, the QA Test Plan artifact and the Review Log, each at its own
destination or presented as not persisted (the existing write policy).

* I1: revert the plan-eng-review self-check wording; it did not change the artifact outcome

A local run of plan-eng-review-artifact in the CI image (Claude Code 2.1.284)
at e2f65ab22 still wrote one review-output.md in 8 turns and ran no Bash, so
the QA Test Plan artifact block never ran. Keep the skill text unchanged; the
case is deferred with its evidence.

* I1: seed the Codex files' periodic durations from their first executing run

Every Codex file recorded ~1 s because its commands could not start in the
container (codex-e2e*, outside-voice, workflow). Run 37162480720 executed them
on the host-run job; its slice 24 walls replace the placeholders so the
planner packs the Codex slice by real work (7 files, ~4.9 min estimated).

* Refresh scripts/free-test-durations.json (bun run test:ubicloud --record-durations; covers the wave's new test files)

* evals2 follow-ups: outside-voice-evidence helper in five case touchfiles; close the evals lane's Codex-CI, late-input, batching and brain-writeback TODOs (run 37162480720); CHANGELOG lines

* fix(browse): take the next free X display when a concurrent allocator wins the race

Pick-then-spawn let two allocators (parallel daemons or test shards) choose
the same free display; the losing Xvfb exited and the handoff failed (run 6,
handoff 'failure before launch'). spawnFreeXvfb now treats a display taken
during startup as a collision and moves on. Regression test forces the race
with a delayed Xvfb wrapper (pre-fix: fails; two-process repro 13/15 -> 0/15).

* test(ci): read the host-run Codex eval job's docker env, mounts and uploads

eval-codex-slices runs on the host and passes its executor env through docker
-e flags and HOME through a $RUNNER_TEMP/eval-home mount. The evidence and
coordination tests now map those back to the container view instead of
assuming job.container, and the job's native-capture upload carries the
qa-callers paths like the other executors.

* I1: plan-eng-review-artifact runs the real interactive review in a PTY, replacing the composing fixture

The claude -p case told the model to skip the preamble and questions and go
straight to the review. In 37151477069, 37158847998, 37162480720 and a local
CI-image run it wrote the whole review into one file without executing the
workflow, so it measured composing, not /plan-eng-review. The interactive
batching PTY run in 37158847998 did write the artifact during Test review, so
there is no product defect.

New test/skill-e2e-plan-eng-artifact.test.ts keeps the case id, tier
(periodic), kind (rule) and budget (CAPTURE_LONG_MS): it drives the real
/plan-eng-review over the same dashboard plan and sources, answers each
question with its first option, stops once the QA test plan exists or the
report completes, and asserts the same requirement (exactly one new
eng-review test plan in the run's own state root, about the dashboard change).
The composing case is retired from skill-e2e-plan.test.ts in this commit.
runPlanSkillCounting reports engTestPlans (read before fixture cleanup) and
passes them to isCollectionComplete. One paid run in the CI image (Claude Code
2.1.284): pass, collection_complete at 498 s with one test plan.

The new file takes its own periodic runner (seeded at 498 s), so the periodic
census plans 26 slices: the planner's --max-parallel and both executors' caps
move from 25 to 26 so every slice still starts at once; the paid-file census
pin moves from 106 to 107.

* evals2 final: plan-eng-review-artifact touchfiles point at its PTY file and helpers; plan file wall drops the moved case

Applies the lane's touchfiles-data patch (its outside-voice-evidence hunks
were already in 9a96fb3). skill-e2e-plan.test.ts lost one 600 s case to
skill-e2e-plan-eng-artifact.test.ts, so its periodic wall is two 900 s + two
600 s (3,000,000 + reserve) and its timeout pin lists the remaining cases.

* TODOS/CHANGELOG: close plan-eng-review-artifact (real PTY review, 498 s pass)

* test: plan-eng-review-artifact is a PTY runner consumer (selected by claude-pty-runner and pty-screen changes)

* test(windows): spawn cmd.exe hooks verbatim, use a .invalid UNC host, and match CRLF fixtures and native separators

windows-free-tests reds on 364f0f3, each verified on windows-latest
(46 pass, 1 POSIX-only skip, 0 fail):
- investigate-freeze-path cmd.exe cases: Bun MSVC-quoted the command line
  cmd.exe parses, so cmd saw \"bash and failed; Node's shell spawn (what
  Claude Code uses) passes it verbatim.
- investigate-freeze-path UNC cases: a single-label host made each
  filesystem probe wait on network name resolution (cd 11 s, -L 2.8 s);
  a reserved .invalid host fails at once (55-80 ms).
- paid-report-census: the junit fixture is CRLF in a Windows checkout.
- codex-invocation-sites: rendered paths use backslashes on Windows.

* eval(plan-design-with-ui): the Step 0D focus title may spell the seven dimensions as a word

Run 37166586458 slice 5 (dfaf154) asked 'D2 - Review all seven design
dimensions, or focus on specific areas?' and reached its review finding,
but the focus predicate only accepted the numeral, so the case failed with
no acknowledged Design focus. Both recognizers accept 7 or seven; the
byte-exact capture is a free regression, and engineering/other-count/
no-'design' titles stay rejected.

* eval(ship-docsync): git check-ignore is a declared read in the docs observation interface

Run 37166586458 slice 2 (dfaf154): ship-docsync-completion's parent ran
'git check-ignore -v .qa-state' to verify the prompt's claim that the
observer probe is Git-excluded. The read was outside the closed Git list,
so the run's DOC_PATH replacement was unprovable and failed. check-ignore
cannot write; it joins the list and the interface prose. Free regression
replays the captured command against the fixture and keeps composition and
redirects rejected.

* fix(browse): read Xvfb's own startup error to classify a lost display race

Run 10 (1ffd38e) failed handoff's concurrent promotion once: 'Xvfb on :100
exited during startup (code 1)' with the display free again by the exit
check, so a holder that won the lock and closed within the 100 ms poll
looked like a broken install. Startup stderr now goes to a private file;
'Server is already active for display' is a collision and moves on, and any
other startup exit carries Xvfb's fatal line. Regression: a stub that
reports the collision for one display and exits leaves spawnFreeXvfb on the
next display (pre-fix: the misleading install error).

* test: merged-main fixtures (E6 copies the Claude render library gstack-config now sources; the census fixture's plan-mode-no-op budget follows its v1.91.18.0 policy)

* qa: materialize runs once per PROBE_DIR, and qa-only says §1 reuses the REPORT_DIR it resolved

Run 37170610789 slice 3 (feaa28d): the qa-only workflow judge scored
clarity 3.33 and named two ambiguities: §4 said materialize runs once per
report root while mixed runs materialize each surface's PROBE_DIR, and
SKILL.md deferred exploratory §1 without saying §1 reuses the REPORT_DIR
resolved under Prepare Report Artifacts. A local run of the same judge on
the repaired text: clarity 3.67, completeness 4.33, actionability 4 (pass);
all three samples call the workflow internally consistent.

* Regenerate generated output for the qa materialize/REPORT_DIR wording

* eval: three detector defects from run 37170610789 (feaa28d)

- safety-codex-consult-embed: the fake codex recorded argv only, but every
  codex exec takes its prompt on stdin since E5; it records stdin for the
  '-' form and the check reads both.
- ship-docsync-missing-asset: 'ls ... 2>&1' on a read-only command is the
  same harmless stderr wrapper as '2>/dev/null'; stdout redirects, pipes
  and script calls stay rejected.
- qa-functional-webhook-fix: a pre-repair capture whose command reran on
  the repaired inputs may be classified superseded (exploratory §4); the
  evidence check accepts that and still rejects superseded on a post-repair
  capture or one never rerun.
Each free regression fails without its repair.

* fix(test:free): a browser record removed mid-read by a shutting-down daemon is absent

Run 13 (feaa28d) needed the flaky-retry for 'owned detached browser
settlement > cancel': 2 unattributed failures instead of 1. A 16-vCPU
stress run reproduced it once in 64 with the cause:
'browser ownership unavailable'. On SIGINT the cancelled daemon removes its
state file; record() saw it exist, then lstat/read hit ENOENT, which
failure() reports as an ownership error. ENOENT there now means absent.
New mode cancel-vanishing-record forces the removal between the check and
the read (pre-fix: 2 unattributed, deterministic; now 1).

* test(windows): the 2>&1 wrapper regression uses relative reads (no native paths in literal commands)

* test(plan-seed): wait for the fixture CLI's slash event instead of a fixed 50 ms

Run 14 (b2f729b) needed the flaky-retry for 'startup-placeholder': the
events log had paste/enter/end_turn but not yet the slash 50 ms after it
was sent. The check now polls (bounded 5 s); the deadline assertion
measures when submission settled, which is what it bounds. One test
typecheck diagnostic goes away with the typed reader.

* eval(plan-design-with-ui): recognize the Step 0D focus question by meaning, not one title spelling

The b2f729b gate run titled it "I've rated this plan 5/10 on design
completeness. Review all 7 dimensions, or focus on specific areas?", the
third native wording to miss a spelling-pinned regex. The native form is
now a class: a title that offers all seven (numeral or word) dimensions or
passes versus a focus and names design. Captured titles from 37166586458
and this run pass; other counts, engineering passes, titles without design
or without a focus stay rejected.

* eval(shared-libs-read-only): curl answers the provider rate-limit probe with the same 404 as gh

Gate census 37174266054 slice 9: the model probed API availability with
curl -o /dev/null https://api.github.com/rate_limit (GET, discarded). The
gh transport already answers rate_limit as an unknown endpoint (404); the
curl transport refused it, and the refusal counted as a read-only
violation. curl now gets the same 404. POST, file output, other repos and
raw hosts stay refused. Paid rerun (gate, 1 trial): PASS in 177 s.

* eval(shared-libs-plan-callers): an exclusion may combine states (unchanged/out of scope)

Periodic census 37174266054: every option ended 'Scheduler semantics,
existing callers and helper hardening stay unchanged/out of scope', which
the actor read as proposing hardening. Exclusion states may now be joined
by '/', 'or' or 'and'; the captured question is answered, and clauses that
pair a state with a real change ('stay unchanged/harden the helper') are
still refused. Paid rerun (periodic, 1 trial): PASS in 296 s.

* paid: --case plan-eng-review-artifact selects its PTY test by its registered name

A local --case run matched 0 tests (no spend): the PTY test's Bun name is
not its case id, and CASE_TEST_NAMES had no label for it.

* eval(plan-eng-review-artifact): periodic checkpoint resumes at Test review; the fresh full run moves to marathon

A fresh interactive /plan-eng-review cannot reach Test review inside the
600 s periodic budget: startup plus one-at-a-time Scope Challenge questions
took 595 s with six questions and no Test review in census 37174266054 and
in a local run (one earlier fresh run passed at 498 s). Per the eval-speed
policy, the long run moves to the non-blocking marathon lane and the
blocking case becomes a focused checkpoint.

- plan-eng-review-artifact (periodic, 600 s unchanged): resumes the real
  interactive skill from a saved review record: the plan, the Scope
  Challenge result, Sections 1-2 complete, and a Decision ledger rebuilt
  from that census run's six actual native questions and answers
  (test/fixtures/plan-eng-artifact-resume-37174266054.json). The live skill
  runs Section 3, Test review. Assertion unchanged: exactly one new
  eng-review test plan in the run's own state root, about the dashboard
  change. Paid trial (local, Claude Code 2.1.284): PASS, 364 s.
- plan-eng-review-artifact-full (marathon, PTY_LONG_MS): the fresh
  start-to-finish run with the same assertion; budget measured beside it.
- test/plan-eng-resume.test.ts proves the rebuilt record is a valid resume
  point and rejects missing, unanswered, edited or duplicated records.
- Registry: touchfiles, tiers, kinds, test-name labels, durations, paid
  file count 115, PTY consumer list; CHANGELOG and TODOS.

* setup-deploy: say which deploy fields count as confirmed, how to resolve the default branch, and group the detection tests

Periodic census 37176837432: the setup-deploy workflow judge's actionability
mean was 3.67 (4, 4, 3). The low sample named the Fly/Heroku/Railway/GitHub
Actions deploy trigger left undefined until Step 4's 'not already confirmed'
catch-all and an unstated way to find <default branch>, and misread
'A || B && C' detection lines. Step 4 now defines a confirmed field and names
the paths that leave the trigger unset, <default branch> resolves with
gh repo view, and the two OR'd detection tests are grouped explicitly (same
behavior). One judge run on the repaired text: 4/4/4, pass.

* Regenerate generated output for the setup-deploy wording

* TODOS: census data for the skipped CEO mode handoff (mode-routing, auto-decide-preserved); floor runner Edit-permission parse under collapsed TTY spaces

* fix(browse): a blocked navigation that fails at once leaves the tab blank and the next command clean

PR CI on 8d2d269 (free-suite 3): the redirect to [::ffff:169.254.170.2]
failed immediately on the runner, so its chrome-error page committed around
the guard's about:blank reset, and leaving the error page committed a
second about:blank after the reset resolved, interrupting the next three
tests' gotos. The guard now lets the blocked request settle (bounded at
1 s; a hanging target is still cut off by the reset), resets, and waits for
the main frame to stay quiet before returning. Regression: a redirect to a
blocked address on Chromium's unsafe port 1 fails at once (pre-fix: blank
check and the next goto fail, deterministically).

* qa: the checkpoint note is created for browser runs and composed for functional ones

Periodic census 37178143007: the qa-only workflow judge scored
actionability 3.67 again; one sample read step 2's 'Create
exploration-NNN.json' as applying to functional runs before reaching
'Functional: do not write this file'. The sentence now names both surfaces
up front; byte-neutral (the browser line drops its restated clause), qa
union 1.1098 under its 1.110 cap. One judge run on the repaired text:
3.67/4/4, pass.

* Regenerate generated output for the qa checkpoint wording

* eval(section-loading): byte-range prints of a section count as a complete read

Periodic census 37178143007: plan-ceo-section-loading printed
review-sections.md in five byte ranges (head -c 16000, then tail -c +N |
head -c 17000, one-byte overlaps) and the detector, which matched whole
printed lines, saw the lines split at each range edge as missing. Outputs
of the commands that name the section are also joined on their overlap.
Regression: the captured ranges count; dropping a middle range or the last
range does not (pre-fix: the complete ranges were refused).

* test(plan-design-review): count design terms the review wrote into the plan

* fix(aside): probe names the platform (GSTACK_PLATFORM override) so the macOS pitch never re-probes the OS

* fix(plan-ceo-review): Step 0E mode handoff runs gstack-ceo-mode-handoff so the selected mode is printed, not only composed

* chore: refresh context-budget fixture

* test(auto-decide): accept Step 0E's documented AUTO_DECIDE line with its decisions suffix; replay the captured trial

* docs: CHANGELOG and TODOS for the CEO mode handoff and platform line

* test(plan-design-review): plan-term credit needs a rated summary; adapter regression for census 37179171083

* test(auto-decide): the printed handoff may precede Step 0E's provenance log; replay census 37182865432; TODOS for two rotating census reds

* test(plan-ceo-section-loading): read the skill's ledger and currentDecision records for the stale-fill decision; replay three census reports

* test: $B counts as a browser call only in command position; qa functional prompt defines its classification values

* test(shared-libs-plan-callers): a verbless unchanged-scope list is an exclusion; replay census 37186854666; TODOS for two census reds

* fix(settings-hook): retry a lock released between mkdir and the existence check instead of dropping the mutation

* test(ship-docsync): the child's reply to a parent SendMessage counts as its spawned-marker report; replay PR run 37188668421

* test(browse): Xvfb stub-failure tests allocate from ranges no other allocator uses

* perf(ship): documentation gate snapshots and compares its candidate with gstack-docs-candidate

* fix(review): unverified Codex mode runs the passes in the mode summary too; stale-fill decision check accepts any ledger row ID

* test(shared-libs-plan-callers): judge scope by forbidden verb+object actions, not an exclusion grammar; replay six refusals and four passing censuses

* test(shared-libs-plan-callers): type the brief-line parameter

* test(ship-docsync): an inline-code status quote is not a false current claim; replay PR run 37196811165

* fix(review,ship): Codex challenge and structured-review bodies run in unverified mode, matching their headers

* docs(TODOS): overlay-harness control-arm error and format-approach provider refusals, with run links

* docs(TODOS): qa-only workflow judge data point

---------

Co-authored-by: Lewis <lewispeel@hotmail.com>
Co-authored-by: Yugz <yannduzelier@gmail.com>
Co-authored-by: Florian <florian.cleyetmerle@gmail.com>
Co-authored-by: cdecook23 <decook23@gmail.com>
Co-authored-by: mb1810 <michael.berger1810@gmail.com>
Co-authored-by: Koda <koda@edgeviewfinance.com.au>
Co-authored-by: Merit Blake <merit@newsprint.com>
Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
Co-authored-by: pneumorea-ai <pneumorea@gmail.com>
Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>
Co-authored-by: Matt Van Horn <mvanhorn@gmail.com>
Co-authored-by: Infiniteyieldai <infiniteyield.ai@gmail.com>
Co-authored-by: Gabriel Rondon <grondon@gmail.com>
Co-authored-by: Davon Johnson <d.johnson@dayofshow.net>
Co-authored-by: Kevin Grasso <kevin@biblingo.com>
Co-authored-by: Xuan-Ce Wang <261591073+xwang4-svg@users.noreply.github.com>
Co-authored-by: loulanyue <260355617@qq.com>
Co-authored-by: Thomas Reuser <thomasreuser@gmail.com>
Co-authored-by: Bit0ps <alex.dreamscaper@gmail.com>
Co-authored-by: oli548 <312288720+oli548@users.noreply.github.com>
Co-authored-by: Slava <slava.istomin@tuta.io>
Co-authored-by: ryanlaiwy <129729476+ryanlaiwy@users.noreply.github.com>
Co-authored-by: 李翊楠 <yinanli1917@gmail.com>
Co-authored-by: Mammad M. <mammad@digit.az>
Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com>
Co-authored-by: Spooks444 <193448029+Spooks444@users.noreply.github.com>
Co-authored-by: punksterlabs <77217235+punksterlabs@users.noreply.github.com>
Co-authored-by: Jonathan Zhang <topcoder1@Mac-Studio.local>
Co-authored-by: Marek Cermak <marek.cermak@strv.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>
Co-authored-by: neallee <neallee@gmail.com>
Co-authored-by: hardy <932821449@qq.com>
Co-authored-by: afshaker <andrew@phone2.io>
Co-authored-by: cube <frosimanuel@gmail.com>
Co-authored-by: jacob-wang <jiayuw794@gmail.com>
Co-authored-by: miao <miao@macmini-2.local>
Co-authored-by: Jonah Berg <jonah.berg.g@gmail.com>
Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: Ryan Alberts <ryan.a.alberts@gmail.com>
Co-authored-by: AlinValentin7 <AlinValentin7@users.noreply.github.com>
Co-authored-by: Evgenii Lopatin <e75533@gmail.com>
Co-authored-by: Jino <dgfrty00@gmail.com>
Co-authored-by: snig-17 <santosh.m.tiwari@gmail.com>
Co-authored-by: Sholto McNeilage <sholto@nodenarrative.com.au>
Co-authored-by: Phil Hie <philhie@outlook.com>
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: Jimmy Zeisweiss <4261258+jzeisweiss@users.noreply.github.com>
Co-authored-by: liutiming <timingliu@outlook.com>
Co-authored-by: Brandon Pugsley <aibpaem@gmail.com>
Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com>
Co-authored-by: Som Samantray <som.samantray@gmail.com>
Co-authored-by: Vaughan <vaughan@cairnsitguy.com.au>
Co-authored-by: Bharadwaj Mittapelli <bharadwajmittapelli@gmail.com>
2026-10-04 07:58:41 -07:00
ac20ef1e13 v1.91.16.0 fix: safe installs and upgrades, skills start everywhere, truthful completion (parallel wave) (#3014)
* fix(make-pdf,design): real TOC page numbers and anchors; design models move to gpt-5.5 + gpt-image-2 (#2903, #2807)

* ci: pin every action to a SHA, Scorecard, checksummed Bun, scoped Codex CLI in the CI image, macOS named-regressions job (#1948, #1706, #1997)

* fix(cso,browse): Windows/macOS lease identity, retention skips bad runs, truthful 'not assessed' reports, env.kv false positive, Windows browse spawn fixes (#2894, #2912, #2048, #2637, #2805)

* fix(outside-voice): one Codex model precedence for probe and dispatch, bounded timeouts with KILL escalation, probe cache identity, guarded mktemp, skills hidden in nested Codex (#2776, #2914, #2787, #2881, #2847)

* feat(setup): install registry, ./setup --status, staged activation, per-scope migrations, host tiers and conformance kit, OpenCode/Hermes fixes, Copilot CLI (experimental), disabled_skills, slimmer CLAUDE.md (#1925, #2879, #2347, #2338, #2626, #2629, #2825, #2015, #393, #1206, #2096)

Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com>
Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com>
Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>

* fix(preamble): skills start in worktree-isolated Claude Code sessions — one literal skill-start line, context-recovery helper, no eval in skill blocks, $N token tripwire, honest plan-mode precedence (#2763, #2896, #2851)

* fix(setup): per-install renders for renamed, vendored and custom-config installs, space-safe alias roots, per-host /cso launcher path (#1882, #2906)

* test: free tests never write the real HOME (home-write tripwire), no orphaned browse daemons, symlinked-TMPDIR (macOS) fixes, gbrain-on-PATH isolation (#2895, #2816, #2829, #2841, #2982, #2911, #2909, #2908, #2910)

* chore: integrate wave follow-ups — regenerate all hosts, README model docs, TODOS, touchfiles, duration seeds, Bun 1.3 telemetry test fix, replay fixtures pinned to their recorded bytes

* v1.91.14.0: release metadata

* test(browse): group-kill cleanup falls back to the leader when macOS returns EPERM for a live group

* ci: Codex login steps run under bash (the CI container's default sh rejects pipefail)

* fix: full-suite repairs — private state roots for 7 real-HOME writers, truthful home-guard attribution with --attribute-home, gstack-paths shim in ship-hook fixtures, Copilot in team-init probes, test-of-test detector, cookie review fixture pinned to the approved excerpt

* fix(windows): LF for Dockerfiles, normalized home-guard exclusions, path.relative fixture containment, ship push test spawn timeouts

* test(design-detect): widen the whole-scan budget test's timing margins so a loaded macOS runner can't time out the single-batch call

* fix(evals): qa protocol names defined for the judge, async PTY judge so overlapping sessions keep their cleanup deadline, bws setup cases use the standard JUDGE_MS tier

* test(pty): the non-blocking judge case is POSIX-only (its fake judge is a shebang script, like the PTY harness it covers)

* test(evals): ship-coverage-audit names absolute skill paths, uses Read, and skips the Step 7 artifact nothing consumes (removes the mechanism behind 3 of 4 historical timeouts)

* test: deflake terminal-agent concurrent startup (write the placeholder record before spawning, like the product) and qa-evidence resumed pipe (barrier on a full high-water mark instead of a sleep)

* fix(design): daemon log stream handles open errors instead of crashing the daemon; shutdown test awaits the stubbed exit

* test(evals): skillify checks the product's resolved state root (no GSTACK_HOME bypass), negative cases check every root; tpa shims live outside the agent's cwd and answer consistently

* test(evals): coverage-audit detector splits parallel columns at │ rails and [+] groups; coverage prompts name absolute skill paths; TODOs from the gate eval census

* fix(plan-eng-review): scope gate always sends the menu (no headless 'Scope pending' exit); PTY harness ends idle turn-ends and API-error panels promptly with a precise diagnostic

* test(evals): one position-bound coverage-read rule credits read-only && chains (labels, context ranges, git displays in either order) and still rejects forged output

* fix(ship): documentation section has one saved source inserted by cat (never retyped); spawned-marker evidence is the child output; refreshed codex/factory ship goldens

* test(docsync): the byte-exact insert positive is POSIX-only (the bash-command observer refuses backslash paths by design)

* test(evals): plan-design-with-ui focus-title rule accepts the comma-less 'dimensions or focus' wording (replayed on the captured CI run, negative controls kept)

* fix(qa-callers): exploratory preflight says the scope/method Reads come after exploratory.md; design-review scan check credits an executed scan proven by one engine run over the changed files

---------

Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com>
Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com>
Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>
2026-10-03 07:54:09 -07:00
Garry TanandClaude Fable 5 07b59e396c v1.75.0.0 feat: ponytail import wave — simplification review lens, arm benchmark, reuse ladder, instruction-tier digest (#2722)
* feat(autoplan): eng review always runs last — the gate reviews the final amended plan

Reorder the pipeline to CEO -> Design (if UI scope) -> DX (if developer-facing
scope) -> Eng. The old order (CEO -> Design -> Eng -> DX) let DX findings land
AFTER the required gate signed off, so eng validated a stale plan.

Accept-all semantics made explicit: every AskUserQuestion resolves to the
recommended option; premises no longer pause the pipeline mid-run (clearly-wrong
ones queue as User-Challenge items at the single Final Approval Gate). Eng's
Codex voice now sees the DX consensus summary. New free static test pins the
order; the chain E2E gains DX-between and Eng-terminal assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(review): simplification specialist — advisory over-engineering lens with ponytail's tag vocabulary

New 8th Review Army specialist (DIFF_LINES > 100, --simplification force flag)
hunting unrequested STRUCTURE only: delete/stdlib/native/speculative/shrink
closed tags, one-line findings, lines_removable field. speculative: replaces
ponytail's yagni: tag — we import the lens, not the posture; coverage stays
sacred (Completeness Gaps owns it, suppressions inlined, shrink needs >=5 lines).

Advisory carve-out in the merge step: advisory findings are excluded from
quality_score and the findings-count header, render with an [ADVISORY] label,
and are ASK-only in Fix-First. Zero-findings case prints the lens-scoped
'Simplification: lean already — nothing to cut.' from the PARENT (the
specialist keeps the exact NO FINDINGS contract); with findings, the parent
prints 'net: -N lines possible' summed from lines_removable.

Tests: static pins for the carve-out + early-out contract (gen-skill-docs),
two periodic e2e cases with planted fixtures — activation (over-build traps:
hand-rolled Intl, one-impl abstract, dead config) and false-flag precision
(a lean ETHOS 'choose A' diff must yield NO FINDINGS).

Inspired by dietrichgebert/ponytail's /ponytail-review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(preamble): reuse ladder in Search Before Building — rungs 2-5 of ponytail's ladder, completeness kept

Tier-3+ skills gain a per-edit reflex the section only stated as research
discipline: before writing new code, stop at the first rung that holds —
repo helper, stdlib, native platform feature, installed dependency — then
build the COMPLETE version of what remains. The closing clause is the
explicit reconciliation with Boil the Ocean: the ladder governs structure,
never coverage. Rungs 1/6/7 (YAGNI / one line / minimum that works) are
deliberately NOT imported.

Also ports ponytail's root-cause rule: one guard in the shared function
beats a guard in every caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(preamble): bounded-closer output rule for tier-2+ skills

After completing work, skills report in a few short lines — what changed,
what was skipped, what to watch — and cut any explanation that outgrows the
change. Explicit exemptions protect every mandated output: decision briefs,
completion-status blocks, user-requested explanations, and report-shaped
skills' report formats (the report IS the work in /qa-only, /plan-*-review,
/retro, /document-generate).

Rationale is signal-to-noise, not tokens: ponytail's own benchmark shows
terse prose alone doesn't cut cost (caveman arm: -20% LOC, +7% tokens), and
independent replications found its 'skipped on purpose' essays ate the code
savings. Includes a good/bad closer example pair per the model-overlay
guidance that a positive example beats a 'don't be verbose' instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(resolvers): terse-mode savings claim matches measurement — 2.6KB, not 3-5KB

Measured on the v1.71 render: --explain-level=terse saves exactly 2,611 bytes
per tier-2+ skill. The old ~3-5KB claim predated the preamble restructuring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(retro,preamble): gstack-shortcut debt ledger — accepted shortcuts leave a joined trail

When the user accepts an option that is BOTH Completeness <= 7 AND a
durable-scope call, the decision ledger entry (gstack-decision-log, ceiling +
upgrade trigger in the rationale) is the source of truth, and the agent marks
each cut corner in code with gstack-shortcut(dec-<id>): <ceiling>, upgrade
when <trigger> — same edit, no follow-up question, never agent-initiated.

/retro Step 11.5 harvests markers into a debt ledger (grep || true — zero
matches is the healthy case; skill installs and docs excluded), joins on the
decision id so nothing double-counts, tags unlinked and no-trigger rot risks,
and closes with 'N markers, M with no trigger.'

/review suppressions: a marker with ceiling+trigger downgrades a would-be
Completeness Gaps finding to acknowledged debt. Redaction test pins that the
marker ships untouched (the ledger is the point) — it does not match the
TODO(owner) hygiene shape.

Format from dietrichgebert/ponytail's ponytail-debt; store inverted to gstack's
existing decision ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: refresh golden ship baselines after preamble additions (reuse ladder + bounded closer)

The golden-file regression test pins the rendered ship skill byte-for-byte;
the WS3/WS7 preamble sections are deliberate changes, so the baselines
re-capture per the goldens' own update protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(hosts): instruction-only tier — a 2KB committed rules digest any agent host can read

New agents-digest/gstack-AGENTS.md (1,765 bytes, hard 2,048-byte budget):
gstack's ethos one-liners, the reuse ladder, and voice rules for hosts with
no install arm — Zed, Amp, Jules, or any AGENTS.md-reading agent. Generated
by scripts/gen-agents-digest.ts, auto-refreshed by gen:skill-docs, committed
like llms.txt so setup's explainer arms can point at it before any toolchain
exists. First line carries the gstack version as its own staleness nudge.

Delivery is print-path + user-performed copy ONLY: setup never writes or
overwrites a user's AGENTS.md (a test pins this — no cp/ln/mv/redirect into
AGENTS.md anywhere in setup). openclaw and hermes explainer arms print the
path; slate keeps routing to the full Claude install and gbrain ships from
its own repo. HostConfig gains the optional install.instructionTier slot,
declared by both instruction-tier hosts. README host table now matches what
setup actually does.

Inspired by dietrichgebert/ponytail's instruction-tier AGENTS.md fallback —
one generated source, never per-host hand copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(preamble): AskUserQuestion repetition cut — gated, passed NOT-WORSE A/B

Removes the duplicate statements v1.71's compaction left in the
AskUserQuestion Format section: the completeness rule restated in the prose
triad, the auto-decide marker syntax stated twice, the Conductor-flakiness
explanation stated twice, and the self-check's full triad restatement. Every
verbosity floor and all 14 format pins stay (Layer 0 green).

The gate this decision rested on ran before landing (new periodic
skill-e2e-auq-repetition-cut-ab.test.ts, pre-cut ref 3263fffe vs this
render, same harness as auq-verbose-vs-carved-ab): POST 7/7 format elements,
substance 5 — identical to PRE. No degradation; the load-bearing-repetition
hypothesis did not hold for these duplicates.

Net: -236 bytes per tier-2+ skill (~9.7KB corpus). Golden ship baselines
re-captured for the deliberate change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(evals): with-skill vs without-skill arm benchmark — measures whether gstack's behavioral layer earns its tokens

Ponytail's honest-benchmark method pointed at gstack itself: 3 build-shaped
tasks (native-platform over-build trap, CRUD endpoint, bug fix with planted
decoys) x 2 arms, real claude -p sessions, scored on the git diff left
behind. A research instrument, not a release gate — no assertion compares
arm scores.

Arms use the PROVEN project-scope pattern: the with-arm installs a
build-discipline skill (extracted reuse-ladder + bounded-closer content, not
whole-file copies) into the fixture's .claude/skills/ with a CLAUDE.md
routing line and an explicit invocation; a live spike confirmed claude -p
discovers and invokes project-scope skills via the Skill tool (3 turns,
exact-output probe). Fixtures are git init + local bare origin; diff capture
is three lines of git, no worktree machinery.

Failure taxonomy: zero-diff arms are VALID scored cells (deterministic
0/none, no API call), harvest failures record harvest:null, judge_error
cells are excluded from aggregates but named in the report — nothing drops
silently. armJudge: fixed sonnet judge, 0-3 unrequested-structure rubric,
must name the construct or say none, bounded retry-on-malformed; callJudge
gains optional temperature/max_tokens (defaults unchanged). recordE2E now
populates tokens_used for every E2E. Eval schema v2: harvest gains
{insertions, deletions, net}, tolerant reads keep v1 runs comparable.

Registered periodic in E2E_TIERS + touchfiles (with the auq-repetition-cut
A/B); periodic detach timeout raised to the new shard-census floor. Free
selftest (8 tests, zero API) pins fixtures, extraction, arm asymmetry, diff
capture, judge plumbing, and the retry bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: absorb the ponytail-import wave into the guard fixtures — ceilings, schema pin, triad phrasing

Skeleton ceilings re-captured for the 17 carved skills the wave deliberately
grew (reuse ladder + bounded closer + shortcut trail, net of the gated -236B
AUQ cut), each with its measured size in the comment per the carve-guards
protocol. eval-store schema pin updated to v2 (harvest gains
insertions/deletions/net). The AUQ prose-triad keeps its pinned per-choice
phrasing ('explicit on EACH choice') while still deferring the score scale to
the canonical Format rule — the shipped cut is strictly closer to the pre-cut
text than the render that already passed the NOT-WORSE gate. Autoplan carve
anchors follow the Phase 2.5 renumbering. Golden ship baselines re-captured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: observability partial-file pin follows eval-store schema v2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test-runner): GSTACK_FREE_JOBS + opt-in flaky-retry pass for syscall-supervised sandboxes

GSTACK_FREE_JOBS overrides the computed shard count (the free runner's
analogue of the paid runner's EVALS_JOBS). On Vercel sandboxes, PID 1
installs a seccomp filter whose supervisor spuriously fails access(2) for
busy processes — measured: 200/200 git-init probes fail 'Cannot access work
tree: Permission denied' while the suite runs at 6 shards, 0/200 idle;
statx succeeds while access fails on the same path in the same process.
One serial mega-shard maximizes per-process pressure and fails too; 2
shards is the measured sweet spot.

GSTACK_FREE_RETRY_FLAKY=1 (default OFF — dev boxes should see flakes)
re-runs attributed failures once, serially, capped at 5 files; a clean
retry downgrades to a loud FLAKY-PASS naming the offenders, a repeat
failure stays red, timeouts and unattributed failures never retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): portable temp paths — TEMP_DIRS allowlist, tmpdir()-based test files

Local path validation now accepts os.tmpdir() alongside the classic /tmp
(new TEMP_DIRS in platform.ts): on macOS os.tmpdir() is /var/folders/...,
and TMPDIR-honoring CI/sandbox environments point it elsewhere entirely —
both are legitimate scratch space. Remote file serving (TEMP_ONLY) stays
pinned to TEMP_DIR alone; no change to the exfil boundary.

commands.test.ts drops 41 hardcoded /tmp literals for a tmpp() helper on
os.tmpdir() (two message assertions now reference the same variable), and
path-validation's symlink-escape test targets /etc/hosts instead of
/etc/crontab — the target must EXIST for realpath to resolve the link (a
dangling target falls back to the link's own path and passes vacuously),
and /etc/crontab is absent on Amazon Linux.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): portable sha256 — Linux ships sha256sum, not shasum

resolve-user-slug and endpoint hashing exited 127 on Amazon Linux (shasum
is a macOS/perl tool). New _sha256_hex helper prefers sha256sum and falls
back to shasum, matching gstack-verify-gate's existing pattern; both call
sites converted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(next-version): only trust ls-remote when origin is actually configured

Without the guard, git DWIMs the literal 'origin' as an ssh host/path; on
hosts whose transport launders exit codes the probe 'succeeds' with zero
branches and the allocator silently sees an empty queue — the exact
duplicate-allocation failure (#2545) fetchGitClaimed exists to prevent.
git remote get-url origin gates the probe; absence falls through to the
existing local-refs path with its staleness warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(testing): sandbox-doctor — one command makes a cloud sandbox run the suite green

Measured failure taxonomy for Vercel/Conductor sandboxes (missing /dev/fd,
64M /dev/shm, seccomp-supervisor access(2) EACCES under load, uid-1000
processes with FULL capabilities defeating chmod-denial tests, no X server,
no git identity, Conductor git-shim exit-code laundering) plus the
idempotent script that treats all of it and seeds the run recipe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): converge on main's self-contained sha8_of — its tests extract the function standalone

The merge kept a branch-local _sha256_hex helper; main's v1.72 landed the
same portability fix inline WITH tests that extract sha8_of()'s text and run
it under a shim-only PATH — a helper call can't satisfy that shape. Adopt
the landed implementation at both hash sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for GSTACK_FREE_JOBS override and failingFiles attribution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for TEMP_DIRS widening and remote-serving TEMP_ONLY asymmetry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for gstack-shortcut marker grammar and retro harvest joint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for sandbox-doctor shell syntax and idempotency guards

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test-runner): empty-shard outcome carries failingFiles; harden flaky-retry list

The empty-shard early return omitted the (required) failingFiles field —
tsc TS2741 — feeding undefined into the flaky-retry flatMap. Also drop the
dead 'else if (worst !== 0)' guard (the enclosing if already pins it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(release): version-bump write regenerates the version-stamped agents digest

agents-digest/gstack-AGENTS.md embeds VERSION in its first line and is
byte-freshness-gated (test/agents-digest.test.ts + Skill Docs Freshness CI),
but nothing in the release path regenerated it — every version-bumping ship
of this repo would land red. write now spawns the repo's own generator when
present (agentsDigest true/false/null in the output JSON), and ship's
evidence gate allow-lists the digest alongside VERSION/package.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): instruction-tier explainer prints the script-anchored digest path

$(pwd) printed a nonexistent path when setup ran from any other directory;
both arms now share one print_instruction_tier() using SOURCE_GSTACK_DIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(digest): broaden AGENTS.md writer tripwire; pin digest-resolver ladder lockstep

The print-path-only guard now catches tee/install/rsync/dd/truncate, >>
appends, and laundered variable-destination writes. New test ties the
digest's hand-rendered reuse-ladder text to the preamble resolver so an
edit to either fails CI instead of shipping drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(retro): shortcut harvest drops placeholder markers and convention docs

The Step 11.5 grep matched documentation mentions (dec-<id>, dec-*) in
checklists, resolver sources, and convention tests, reporting phantom debt
rows on gstack itself. A trailing filter kills placeholder forms; prose
tells the agent to discard convention-quoting hits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): advisory findings count in per-specialist stats

Without this, simplification (all-advisory by construction) would log
findings:0 every run and auto-gate itself into permanent silence after 10
dispatches. The advisory carve-out governs score and header only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): arm-benchmark harvest and judge hardening

- Harvest diffs against the recorded seed SHA (origin/main is movable by an
  agent that commits AND pushes; a recorded SHA is not).
- Fixtures get a node_modules .gitignore and the git wrapper a 64MB
  maxBuffer, so a vendored-dependency arm is scored instead of killing the
  cell.
- The judge diff cap is a named constant with loud truncation (log +
  judge_reasoning suffix).
- Judge prompt block markers carry a per-call random sentinel, so a diff
  containing a faked closing marker cannot escape the data block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): AUQ A/B vendored pre-cut arm + judge-error inconclusive taxonomy

- The PRE arm read a branch-local SHA (3263fffe) that becomes unreachable on
  fresh clones after the squash-merge; the pre-cut render is now a vendored
  fixture.
- A judge failure on one side no longer coerces substance to 0 (which
  fabricated DEGRADATION on POST-side failures and masked regressions on
  PRE-side failures): null substance = inconclusive, format still gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: regression pin for the originConfigured guard vs laundering git shims

On healthy hosts the guarded and unguarded paths behave identically, so a
revert passes the suite; only a shim that makes 'git ls-remote' exit 0 with
empty output (the Conductor wrapper's observed behavior) exposes it. Pins
that the empty 'successful' probe is never trusted as an empty queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox-doctor): missing /dev/shm no longer aborts the doctor under set -eu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(touchfiles): close dep-list gaps for the new evals

- arm-benchmark entries gain ship/SKILL.md (buildBehavioralSkill extracts
  sections from the rendered ship skill)
- review-army-simplification entries gain their planted fixtures + test file
- auq-repetition-cut-ab gains llm-judge.ts and the vendored PRE fixture

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-capture context-budget fixture — lock the WS6-3 reduction and Step 9 deltas

Per the ratchet protocol: the AUQ repetition cut shrank per-skill eager
tokens but the fixture was never re-captured, leaving the win unlocked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(release): digest regen is an explicit --regen-digest opt-in, not presence-sniffed code exec

Review (security) caught the cycle-1 fix executing any repo's
scripts/gen-agents-digest.ts on plain 'write' — arbitrary code exec from a
hostile clone on a routine bump, contradicting the binary's own containment
posture. The regen still runs the TARGET repo's generator (a 'trusted' copy
beside the binary would false-red the freshness gate on version drift), but
only under the flag: /ship passes it deliberately, in a repo whose code the
operator already executes (its test suite). Plain write is side-effect-free
again. Also: uniform output shape (agentsDigest: null on the JSON-manifest
branch), a REAL generator round-trip test replacing the misnamed lockstep
check, and land-and-deploy's evidence gate gets the same digest allow-path
as ship so the two grading surfaces agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test-runner): flaky-retry vetoes on ANY unattributable failure evidence

The gate equated 'some failure attributed' with 'all failures attributed': a
shard with one attributed failure plus a headerless failure, an unhandled
error between tests, or a truncated run (no terminal summary) qualified for
retry — re-running only failingFiles and masking the rest as FLAKY-PASS,
re-opening the silent-truncation hole the strict classifier closes.
FreeShardOutcome now carries unattributedFailures; nonzero vetoes the retry.
Pins: mixed shard, truncated-with-attributed shard, empty-shard field values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(next-version): a configured origin advertising zero heads is never trusted

The originConfigured guard covered only the no-origin laundering case. With
origin configured (the normal Conductor worktree state), the laundering shim
makes a failed ls-remote exit 0 with empty stdout — read as 'the queue is
empty', the exact duplicate-allocation bug (#2545) one layer up. A reachable
remote always advertises at least its default branch, so an exit-0 zero-head
probe now falls back to local refs/remotes/origin with a laundering-specific
warning. Regression test shims git for both configurations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox-doctor): loud on git-shim patch drift; document the retry-contract override

- The /conductor/bin/git patch was a silent no-op if the shim's bytes drift
  from the exact pattern — now warns that laundering is NOT fixed.
- The bashrc block documents why GSTACK_FREE_RETRY_FLAKY=1 deliberately
  overrides the runner's default-OFF contract on this sandbox, and how to
  undo it.
- Test pins the guarded shm form (missing /dev/shm must not abort set -eu).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(digest): pin the script-anchored explainer path; catch declaration-prefixed writers

- Asserts $SOURCE_GSTACK_DIR/agents-digest path and forbids $(pwd)/agents-digest
  (the cycle-1 fix was revertible without failing anything).
- The laundered-assignment arm now matches local/export/declare/readonly/typeset
  prefixed assignments — the likeliest in-function writer shape in setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(evals): arm-benchmark selftest runs FREE on every PR

The selftest lived inside the paid skill-e2e-* file, so fixture-integrity
and plumbing pins executed weekly at best — a broken fixture would ship past
every gating check and be discovered when the periodic run burned money on a
dead instrument. Harness extracted to test/helpers/arm-benchmark-harness.ts,
selftest to test/arm-benchmark-selftest.test.ts (free suite). Touchfiles:
harness added to the three benchmark dep lists; the auq-repetition-cut-ab
tier comment now states the MANUAL re-run obligation honestly (periodic runs
force EVALS_ALL, so dep lists cannot auto-trigger it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-capture context-budget fixture after cycle-2 template deltas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox-doctor): keep both heredoc bodies under the 512B pipe-deadlock window

The cycle-2 additions pushed the python-patch and bashrc heredocs into the
512-65536B window test/heredoc-pipe-deadlock.test.ts guards (sh scripts get
no BASH_COMPAT escape hatch). Same content, tighter prose; the drift warning
now reuses the patch pattern variable instead of a second literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): a gstack-shortcut marker only suppresses findings when its decision id resolves in the ledger

Cross-model catch (Claude adversarial + Codex agreed): any diff author could
fabricate a marker and silence Completeness review of that gap. Reviewers
now resolve the dec-id via gstack-decision-search; an orphan marker is
reported as a forged suppression, not honored as debt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(autoplan): define the B2 gate path — accepted premise challenges amend the plan and re-run Eng

The final gate offered B2 (respond to User Challenges) but the option
handler table omitted it, leaving accepted challenges with no amendment or
Eng re-review path. B2 now walks challenges one at a time; an accepted one
amends the plan and re-runs Eng (the gate always reviews the final plan),
sharing D's 3-cycle cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(evals): arm benchmark runs each fixture's functional oracle — correctness before LOC

The plan's metric order is diff-quality FIRST, but cells never ran the
fixtures' own run-tests.js, so a refusal, a broken implementation, and
working code were indistinguishable in aggregates (Codex adversarial catch).
Tasks with an oracle declare checkCmd; every cell records checks=pass|fail|none
in the report line and eval store. Selftest pins the oracle declarations and
that the planted bug fails its own check pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ship): check the bump's agentsDigest result; state the --regen-digest trust envelope honestly

A failed digest regen warned and moved on — ship now instructs re-running
the generator and staging the digest with the bump (the freshness check
stays red otherwise). The 'no-op everywhere else' phrasing oversold safety:
the step now names what executes and why that is inside the envelope Step 5
already opened (the repo's own test suite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test-runner): GSTACK_FREE_JOBS accepts digits only — parseInt truncation defeated the loud-failure contract

'2abc' silently became 2 and '3.7' became 3 despite the error text claiming
a positive-integer requirement. Strict /^\d+$/ pre-check; both shapes pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox-doctor): atomic git-shim patch, :99-socket Xvfb check, dnf gate, non-interactive sudo

- The /conductor/bin/git patch writes tmp-then-rename with a .orig backup —
  a concurrently spawned git can never exec a truncated shim.
- Xvfb running-check looks for the :99 socket, not any-display pgrep.
- Xvfb install is dnf-gated so non-dnf distros degrade to a warning instead
  of aborting the remaining fixes under set -eu.
- The bashrc /dev/fd restore uses sudo -n || true — no password prompt at
  every shell start on non-passwordless machines.
- BASH_COMPAT=50 keeps heredoc bodies off the bash pipe window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(build): a failed agents-digest regen fails gen-skill-docs instead of deferring the red to CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): an untrustable TMPDIR (/, $HOME, a cwd ancestor) never widens the local allowlist

TEMP_DIRS honors os.tmpdir() at daemon start; a daemon launched with
TMPDIR=/ would have trusted the whole filesystem for local path validation
for its lifetime. Subprocess pins cover /, $HOME, cwd-ancestor rejection and
that a benign distinct TMPDIR (the sandbox recipe's $HOME/tmp) stays honored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: zero-heads warning names the benign cause too; digest path declaration made load-bearing; ratchet re-capture

- The ls-remote zero-heads warning no longer accuses an empty remote of
  running a laundering shim.
- instructionTier.rulesFile now must equal the generator's DIGEST_RELPATH
  (and setup must print it) — the declaration fails with the real path
  instead of lying silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: file ship-time follow-ups in TODOS

skillify HOME-override gate red (pre-existing, proven on main), the
auq-verbose-vs-carved-ab branch-local ref, eval-store harvest union,
evidence digest allow-path scoping, and the WS6-2 dead-frontmatter live-host
verification deferral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v1.73.0.0 chore: version bump + CHANGELOG — ponytail import wave

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: raise ship skeleton parity ceiling — measured 75,592 after the v1.73 release-step prose

The --regen-digest trust-envelope paragraph (Step 12) and the evidence-gate
digest note (Step 16) grew the ship skeleton past the previous 75,420
ceiling. Re-measured per the deliberate-change protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v1.73.0.0

- README.md, docs/skills.md, AGENTS.md: /autoplan phase order corrected to
  CEO → design → DX → eng (eng always last); /review rows note the advisory
  simplification lens
- docs/PROJECT_STRUCTURE.md: add agents-digest/, gen-agents-digest.ts,
  sandbox-doctor.sh, test-free-shards.ts to the annotated tree
- CONTRIBUTING.md: document GSTACK_FREE_JOBS, GSTACK_FREE_RETRY_FLAKY, and
  the sandbox-doctor one-command fixer in the Tier 1 test section

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: apply cross-model doc-review fixes for v1.73.0.0

- README.md: host table gains the OpenClaw explainer arm row (setup has the
  arm; the table claimed to match setup)
- docs/skills.md: /review completeness-gaps section documents the
  gstack-shortcut(dec-<id>) acknowledged-debt suppression and orphan-marker
  flagging; /autoplan deep-dive states the recommended-option default with
  the 6 principles as tie-breakers
- CONTRIBUTING.md: host count 8 -> 10 (Hermes, GBrain), supported-hosts list
  completed
- docs/TESTING_INTERNALS.md: sandbox recipe says to source ~/.bashrc after
  the doctor seeds it; GSTACK_FREE_JOBS wording fixed from "caps" to
  "overrides in either direction" (matches the un-clamped runner)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): temp-dirs asymmetry pins are topology-aware; TMPDIR probes are POSIX-only

CI exposed two wrong assumptions in the new temp-dirs tests, neither a
product bug:

- The remote-serving asymmetry test assumed a distinct os.tmpdir() lies
  OUTSIDE TEMP_DIR, but the free-shard runner nests each child's TMPDIR
  inside /tmp on CI — a file there is under TEMP_DIR, so serving it
  remotely is legitimate. The test now pins the actual exfil boundary on
  every topology (a cwd project file is locally readable, never remotely
  servable) and branches the os.tmpdir() case on nested-vs-outside.
  Reproduced locally with TMPDIR=/tmp/nested-tmp before fixing.

- The untrustable-TMPDIR subprocess probes set TMPDIR, which Windows
  os.tmpdir() ignores (reads TEMP/TMP) — and on Windows TEMP_DIR is
  DEFINED as os.tmpdir(), so the fixed+movable two-dir topology the guard
  filters does not exist there. Probes now skip on Windows with that
  rationale; the benign-TMPDIR assertion compares realpaths.

Verified under all three POSIX topologies: TMPDIR=$HOME/tmp (outside),
TMPDIR=/tmp/nested-tmp (CI shard shape), TMPDIR unset (identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(build): DIGEST_RELPATH is a forward-slash literal on every platform

path.join built it with backslashes on Windows, so the wiring test's
string comparisons against setup and hosts/*.ts (which carry the
forward-slash literal) could never match there — windows-free-tests red.
path.join(root, DIGEST_RELPATH) at the write site normalizes fine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox-doctor): bashrc block re-heals the /dev/shm remount on sandbox restart

The 4G remount does not survive restarts; a reverted 64M shm made the
multi-tab browse handoff test fail consistently under suite concurrency
(observed live: two consecutive full-run failures, green in isolation,
green again after remounting). Same guarded arithmetic as the doctor body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): close the cross-shard porcelain race that failed Windows CI

Two-part fix for the gen-skill-docs-out-dir isolation-pin failure:

- cookie-import-browser built its scratch cookie DBs inside the TRACKED
  browse/test/fixtures/ dir (created in beforeAll, deleted in afterAll), so
  they flash as untracked files mid-run — a concurrent shard's porcelain
  snapshot caught the window on Windows. The DBs now live in a per-run
  tmpdir; zero source-tree writes.

- gen-skill-docs-out-dir is the free suite's only LIVE porcelain-snapshot
  test, so it joins TREE_MUTATING (the serial quiet window): any concurrent
  transient tree-write can race it, and its own spawned render rewrites
  llms.txt/agents-digest in place (idempotent on a fresh tree).

The race is pre-existing; this branch's +5 test files reshuffled shard
composition and exposed it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v1.75.0.0 chore: queue-advance rebump — perth-v2 landed v1.74.0.0 on main

The v1.73.0.0 slot this branch claimed was superseded when #2721 merged;
same MINOR level relative to main per the versioning invariant. CHANGELOG
entry renumbered (1.73.0.0 was branch-internal and never landed on main),
digest restamped via --regen-digest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test-runner): duration-packed walls keep the per-file floor — predictions don't transfer across machines

The committed duration seed is recorded on fast CI; a syscall-supervised
sandbox replays the same files 2-4x slower. Observed post-merge: a 253-file
shard predicted ~242s was wall-killed at its predicted-x3 725s wall while
genuinely progressing (the old count heuristic guaranteed 1265s). Packed
walls may be looser than the count floor, never tighter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 10:10:35 -07:00
b1485d8897 v1.74.0.0 test/CI overhaul: green means green, suites restructured for speed (#2721)
* fix(ci): free-tests lane actually runs the make-pdf e2e gates

The 9 make-pdf/test/e2e gate tests probe make-pdf/dist/pdf,
browse/dist/browse, and the diagram-render bundle, then self-skip when
absent. The required free-tests lane never built any of them, so the
gates silently skipped on Linux for their entire life (verified: 9 of
14 skip, exit 0). make-pdf-gate.yml's justification for deleting its
Linux leg claimed the free lane covered this — it didn't.

- new build:gates script: exactly the three artifacts the gates probe
  (full bun run build compiles five binaries; ~60-90s tax on the only
  required check is not warranted)
- free-tests.yml: build:gates step + poppler-utils +
  fonts-noto-color-emoji (fonts must precede the first browse daemon
  launch — Chromium snapshots fontconfig at startup; verified live:
  a warm daemon renders tofu, a fresh one embeds NotoColorEmoji)
- make-pdf/test/e2e/ci-prereqs.test.ts: GSTACK_EXPECT_BINARIES=1 (set
  by the workflow) inverts the skip polarity in CI — dropping the
  build step or poppler fails the lane instead of re-opening the
  silent-skip hole

Pre-flight: all 9 gates green on Linux locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): kill the three zero-test eval jobs (hollow green)

- delete the vestigial e2e-codex / e2e-gemini matrix rows: both files
  are whole-file periodic-tier, so with no row tier: they ran ZERO
  tests and reported green on every PR (~2 min of runner each, pure
  false confidence; the periodic lane owns those suites)
- e2e-pty-plan-smoke gains tier: gate — its two files are whole-file
  describeE2ETier('gate'), so the job burned ~7 min of container setup
  then skipped every describe
- KNOWN_TIER_UNSET burned down to empty; the ratchet stays armed so a
  future row/file tier mismatch fails the suite instead of shipping
  hollow green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): least-privilege permissions + fork-safe concurrency keys

- evals.yml / evals-periodic.yml evals jobs: explicit contents:read +
  packages:read (container-image pull) and persist-credentials:false —
  the jobs that execute PR-authored code with three provider API keys
  ran on the repo-default token grant with the token written into
  .git/config
- permissions blocks for the 4 workflows that had none (skill-docs,
  make-pdf-gate, windows-free-tests, windows-setup-e2e)
- fork-safe concurrency keys: actionlint, skill-docs, make-pdf-gate,
  windows-setup-e2e switch from head_ref to PR-number keying — a bare
  branch name carries no fork prefix, so same-name branches from two
  forks shared one group and cancelled each other's runs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): one bun version everywhere + drift tripwire

Lanes disagreed four ways: 1.3.13 (free-tests, windows, Dockerfile.ci),
latest (quality-gate, make-pdf-gate), unpinned (skill-docs,
version-gate — setup-bun installs latest), 1.3.10 (.gitlab-ci.yml).
Different Bun versions change the runner output shapes the strict
classifiers regex-match, spawn semantics, and shell parsing — a lane on
a different Bun tests a different product; Dockerfile.ci's own comment
records this class biting once already (silent 1.3.13/1.3.14 drift).

All surfaces pinned to 1.3.13; test/bun-version-drift.test.ts scans
every workflow setup-bun stanza + Dockerfile.ci + .gitlab-ci.yml and
fails on any mismatch or unpinned stanza. skill-docs also gains
--frozen-lockfile (was bare bun install).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ci): bind the three-way image-tag hashFiles() expressions

evals.yml, evals-periodic.yml, and ci-image.yml each compute the CI
image tag from hashFiles('.github/docker/Dockerfile.ci', 'bun.lock',
'patches/**') — synced by comment only (TODOS.md 'CI three-way
image-tag drift'). If one input list drifts, that workflow computes a
different tag for the same content: eval lanes silently rebuild the
image every run, or ci-image prebuilds a tag nobody looks up. The test
extracts each tag-computation site and fails on any mismatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): ci-image stops rebuilding the identical image every ship

- package.json out of the trigger paths: the tag hash deliberately
  excludes it (version bumps every ship), so every merge rebuilt and
  re-pushed the IDENTICAL tag (~2m26s for zero content change);
  patches/** added (it IS a tag input)
- manifest existence check (mirrors evals.yml): tag already exists →
  skip the build
- concurrency group: two rapid main pushes raced pushing the same
  :latest/:buildcache tags
- cron staggered 06:00→04:00 Monday: it shared the exact minute with
  evals-periodic, which could race a half-pushed tag or duplicate the
  build
- timeout-minutes: 30 (was unbounded → 360-min default for a hung
  docker build)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): quality-gate drops the 74s full-history checkout

fetch-depth:0 cost 74 of the job's 92 seconds; the three gates it feeds
take ~12s combined. Shallow checkout + exact-SHA fetches for the diff's
base/head (an exact-SHA fetch, not a guessed depth — long-lived
branches and merge queues still resolve), with a --deepen fallback for
push events whose 'before' is unusable. timeout right-sized 20→10 min.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): small-lane batch — timeouts, right-sizing, windows cache warm-start

- timeout-minutes on the 6 remaining unbounded jobs (actionlint 5,
  skill-docs 10, version-gate 10, make-pdf-gate 15, pr-title-sync 5,
  evals build-image 15) — a hung step sat on GitHub's 360-min default
- right-size measured-over-long timeouts: dependency-review 10→5,
  windows-setup-e2e 15→10
- dependency-review: 2-core runner (28s API call on an 8-core box) and
  drop .github/workflows/** from its trigger paths (workflow edits have
  no dependencies to review)
- windows caches gain restore-keys: a lockfile bump paid the 26s/43s
  restore for a guaranteed cold miss

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): scope GSTACK_HOME to each file's execution window

Five files assigned process.env.GSTACK_HOME at module scope. Shard
processes evaluate sibling modules before running their tests, so the
assignment leaked into every other file in the shard — the damage was
already visible in defensive workarounds (relink.test.ts:28 'fresh
install test saw a neighbor's skill_prefix'; cdp-e2e's own comment
documents a sibling's temp dir baked into artifacts).

Pattern: save original, assign in beforeAll, restore in afterAll
(cdp-e2e already restored but still assigned at load — its window now
matches the others). GSTACK_TELEMETRY_OFF and GSTACK_PROJECT_SLUG get
the same treatment where they rode along. Victim files' defenses stay
in place (cheap insurance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: tripwire against module-scope GSTACK_HOME assignments

Column-0 assignment of GSTACK_HOME / GSTACK_STATE_ROOT in any tracked
*.test.ts fails with the file:line and the fix (beforeAll + afterAll
restore). Kills the cross-file env-leak class the previous commit
swept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): e2e-harness-audit derives its skill census from disk

The hand-maintained 39-name SKILL_GLOBS list had drifted to 39 of 54
SKILL.md.tmpl on disk. No live gap today (none of the 15 unlisted
skills is interactive), but the next interactive skill would have
landed unguarded with zero signal. The audit now walks top-level dirs
for SKILL.md.tmpl (statSync so symlinked dirs like connect-chrome
count), so new skills are in scope the commit they appear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): judges honor the eval-model resolution chain + real 429 backoff

callJudge inlined GSTACK_EVAL_MODEL_JUDGE || sonnet, silently ignoring
the global GSTACK_EVAL_MODEL override every other eval call site honors
via lib/eval-model.ts. New 'judge' kind in DEFAULTS (sonnet — the D1a
pin-on-regressors calibration stands; model CHOICE unchanged) and
callJudge resolves through it: explicit arg > GSTACK_EVAL_MODEL_JUDGE >
GSTACK_EVAL_MODEL > default.

429 handling upgraded from one fixed 1s retry (reliably lost races at
CI concurrency) to three jittered exponential retries (~1s/4s/16s),
honoring the server's retry-after when present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): the two expect(true) paid stubs become test.todo

skill-e2e-spec-execute (600s budget) and skill-llm-eval-spec (300s)
reported PASS on every periodic run while asserting nothing. Deleting
them would remove the periodic-tier selector surface they exist to
register (diff-based selection for spec/ changes), so they become
test.todo — reported as todo/skip, never pass — with the v1.1
implementation specs kept in-file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): reactivate 5 quarantined browse tests (2 security)

extension-sender-auth's two privileged-message denial tests (content
script + missing sender.url — the extension's security boundary) and
snapshot's three skips were quarantined 'pre-existing' failures. Root
cause: machine-local state on the quarantining dev machines — the test
and gate code are byte-identical between the quarantining commit
(410b4928) and HEAD, and all five pass deterministically on a clean
checkout (68/68 across both files, multiple runs). No assertions
weakened, no product changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): activate the 4 paid test files that could never run anywhere

carve-section-loading, codex-e2e-plan-format,
codex-e2e-recommendation-substance, and llm-judge-recommendation gated
on EVALS/tier (free suite loads them as describe.skip) but their names
fell outside PAID_TEST_GLOBS, so no paid lane ever selected them — net
execution zero, forever. The existing matrix tripwire filtered on
isPaidTestFile() first, so it was blind to exactly this class (the same
bug that hid the pre-split monolith's gate tests for ~8 releases).

- PAID_TEST_GLOBS: codex-e2e* + skill-llm-eval* wildcards (replacing
  exact names) + llm-judge-recommendation + carve-section-loading;
  package.json's six test-script glob lists mirrored
- codex-e2e-plan-format gains the explicit periodic tier gate its
  siblings carry (external-service rule) — without it the sharded
  runner's no-guard default would spawn Codex in the gate tier per PR
- eval:bg:periodic --timeout 32400→37800: the census growth pushed the
  periodic worst case to 35910s; the old value had 270s of headroom
  BEFORE this change and would now kill healthy runs mid-flight
- new test/paid-orphan-tripwire.test.ts: any EVALS/tier-gated test file
  outside the globs fails the free suite (reasoned SCANNER_EXEMPT for
  the gate helpers + meta-tests) — the class-killer
- paid-shards pins updated: the four orphans now assert INSIDE the
  census

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): restrictDirectoryPermissions warns and skips symlinked dirs

Closes the Windows Free Tests red: recent lane failures showed a
platform-unguarded POSIX mode-bit assertion ('Expected: 493' — a
symlink-skip test) from PR-branch variants; the KNOWN_WINDOWS_SAFE
force-include reason ('mode-bitmask hits are POSIX-branch only') did
not hold for that shape, and main had neither the guard nor the
behavior.

- product: lstat first; a symlinked dir gets a warning and a skip on
  both platforms — chmod AND icacls dereference the link, so
  restricting through a symlink hardens an unvetted target (and
  /inheritance:r could lock out its real owner). All callers already
  treat hardening as best-effort (try/catch).
- test: the symlink regression test, platform-aware — symlinkSync in
  the house try/catch skip pattern (Windows runners without Developer
  Mode can't create symlinks), mode-bit assertion guarded off win32,
  behavior assertions (no throw, warning text, target readable)
  everywhere; POSIX still proves the skip (0o755 unchanged, not 0o700)
- KNOWN_WINDOWS_SAFE reason updated to the now-true premise

20/20 pass on Linux.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): unique tmp dirs for plan artifacts + audited live-repo cwd sites

Six paid PTY tests wrote their expected plan artifact to a FIXED shared
/tmp path ('/tmp/gstack-test-plan-<mode>.md') and rmSync'd it in
finally — under --retry 1, EVALS_JOBS>1, or two concurrent worktrees, a
sibling's cleanup deletes this run's artifact and the D19 'agent did
not produce expected plan file' assertion fires spuriously. Each test
now mkdtemps its own dir, interpolates the unique path into the agent
prompt (fixture-sourced prompts get a replaceAll + drift guard that
throws if the fixture's literal ever moves), and cleans up its own dir.

The 18 cwd:-into-the-live-repo sites were audited: all deliberate
(skill registry + hermetic pre-trusted dir, in-repo gen renders, git
history reads, slug resolution) — each now carries a
'// LIVE-REPO CWD: <reason>' comment so the next audit can tell
deliberate from accidental.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): trim the seven over-wall 1700s timeouts to the 1500s physical ceiling

1,700,000ms (28.3 min) exceeded every wall these tests run inside: the
25-min CI job timeout and the 1800s sharded-runner wall (which also
leaves --retry 1 zero room for a second attempt). Budget above the wall
is fiction, not headroom — a test that actually used it produced a
job-level kill (no bun summary, no artifact) instead of a clean
per-test timeout. No recorded p95 exists for this family (they are
being retiered to periodic in the re-platform wave); the trim stops at
the physical ceiling rather than guessing lower. Final policy lands in
the Wave-2 eval-budgets constants module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(gen): main() guard — importing gen-skill-docs no longer regenerates the tree

The generator's whole body executed at module load, so any import of it
(test/gen-skill-docs.test.ts pulls assertSinglePreamble via require();
test/catalog-trim.test.ts imports helpers) regenerated all 71 SKILL.md
in place — the root cause of half the TREE_MUTATING serial-shard
entries (hazard class #2532). The body now lives in an exported
main(): number behind if (import.meta.main).

Semantics preserved exactly: failure exits are immediate (matching the
old top-level process.exit), success leaves the event loop to drain so
the llms.txt fire-and-forget IIFE finishes its write, and the module
stays synchronous/require()-able. Proofs: byte-identical --host all
output (git status clean), --dry-run stale-tree still exits 1 (the
skill-docs freshness lane depends on it), and the new
test/gen-skill-docs-import-purity.test.ts pins load-time purity via a
subprocess probe (mtime-based, so a dirty worktree can't false-fail).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(gen): --out-dir renders every host, outputs-only

--out-dir was Claude-host-only (gen-skill-docs.ts:842), which forced
the codex/factory-regenerating tests (gen-skill-docs, skill-validation,
host-config) to mutate the live tree — the reason they sit in the
TREE_MUTATING serial shard. The flag now mirrors ALL outputs into the
out-dir: external-host trees (.agents/.factory/... via
processExternalHost), external section files, openclaw docs, and
gstack/llms.txt (a catalog-mode render must never rewrite the tracked
index). OUTPUTS ONLY — inputs (templates, sections/, host configs) are
always read from ROOT, so an empty out-dir can never feed the render.
rewriteSectionBase stays Claude-only (external hosts have their own
path grammar).

Proofs: in-place --host all is byte-identical (tree clean);
--host all --out-dir <mkdtemp> renders the full multi-host tree with
ROOT untouched; gen-skill-docs-out-dir tests + 415/415
gen-skill-docs.test.ts green (bin/dev-setup's claude rendering
byte-compat).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evals): every E2E key's dep list names its own declaring test file

129-of-177 keys omitted their own test file, so editing only a test's
prompt or assertions selected NOTHING — the changed test never ran on
the change that changed it. 135 keys self-registered (110 E2E + 25
LLM-judge), resolved by strict declaration evidence (testName:/
testIfSelected/judge call sites), with skill-name false positives
excluded.

e2e-tier-alignment's warn-only branch for unregistered files is now a
hard failure with a 4-entry KNOWN_UNREGISTERED ratchet (template-
literal testNames, fail-open-safe) + a burn-down test so the set only
shrinks. Selection sanity: a one-file diff on skill-e2e-qa-workflow now
selects its 4 tests (was 0); skill-llm-eval 0 → 25.

Known follow-ups (filed): 15 E2E + 2 judge PHANTOM keys select tests
that exist nowhere; codex-e2e-plan-format's testIfSelected names have
no map keys (run-all only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(evals): ratchet the 8 newly-visible gate-matrix gaps

The self-registration sweep made these eight files' gate-tier keys
visible to the census for the first time — their gate tests run in NO
CI lane today (pre-existing hole, newly measurable). Ratcheted into
KNOWN_MATRIX_GAPS with the burn-down note: the paid-lane re-platform
runs every gate file by construction and retires this ratchet class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): duration-aware LPT shard packing for the free suite

Hash sharding balances file COUNTS (1.15x spread) but not cost — the
Playwright-launching files landed 4/3/4/1/2/1 across 6 shards, giving a
measured 28s–97s shard spread and ~40s of idle tail on every run.
Full-suite mode now packs by recorded per-file durations
(longest-processing-time-first) when the committed seed
scripts/free-test-durations.json exists.

- ONE store, no overlay: the seed is refreshed occasionally via the new
  --record-durations mode (each file timed in its own child — exact,
  and immune to bun's stream buffering, where silent passers print no
  header to timestamp); GSTACK_FREE_TEST_DURATIONS overrides the path
  for experiments; CI never records
- seed is a hint: missing → silent hash-shard fallback; corrupt (bad
  merge) → one warning + fallback; unknown files → 75th-percentile
  pessimism so a surprise long-runner can't recreate the tail
- packed shards get duration-aware walls (max(base, predicted x 3)) —
  LPT decouples count from cost BY DESIGN, so the 5s/file heuristic
  would undersize a shard holding few expensive files
- one log line per shard (files + predicted seconds) so packing
  regressions are diagnosable from any run log
- the --shard CI-matrix path is untouched: stable hash indices are its
  contract
- successor note in-code: bun >=1.3.14 ships native --timings/--shard
  LPT — swap this packer when the repo unpins 1.3.13

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): decouple slop:diff from bun run test; quality-gate runs it per PR

'bun run test' silently appended up to two 120s npx slop-scan runs plus
a git worktree add/remove after the suite (2>/dev/null || true) —
invisible in the documented '~90-100s' timing and pure friction in the
pre-commit loop. Decoupling is not coverage removal: quality-gate.yml
now runs slop:diff on every PR (advisory, matching its in-repo 'never
blocking' contract), and /review already invokes it explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): eval-budgets timeout tiers + fit/ceiling policy test

Five named tiers (JUDGE 120s / CAPTURE 300s / CAPTURE_LONG 600s /
PTY 900s / PTY_LONG 1200s) replace hand-ratcheted sprawl (46x300s,
46x120s, 44x360s, 44x180s, 27x240s, 19x150s, 13x420s, 12x600s...),
much of it inflated to paper over the old 40-way in-shard concurrency
that the sharded runner's 1-file-per-shard model kills. Policy test
pins: every tier fits the shard wall minus 120s overhead (the
structural fix for budgets-above-the-wall fiction), tiers stay ordered,
and no paid literal exceeds PTY_LONG x1.25 — oversized tests get split,
not budgeted past the wall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): shared runBin helper for bin-script unit tests

~36 free test files each carry a near-identical local run() (spawnSync
+ utf-8 + {status, stdout, stderr}) differing only in env composition,
cwd, and timeout. runBin absorbs the invariant core; options carry the
variance (gstackHome sets BOTH GSTACK_HOME and GSTACK_STATE_DIR — the
config-precedence trap several locals rediscovered independently; home
for $HOME-anchored bins; input/trim/timeout/maxBuffer). Free-test-only
by design so it never becomes a de facto global touchfile. Migration of
the 36 call sites lands separately (mechanical batches).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): runBin trim assertion — trim shapes stream ends, not interior

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): mechanical sweep — 298 paid-test timeouts onto eval-budget tiers

69 files, both shapes (trailing bun-test budgets and runner
timeout/timeoutMs options), ROUND-UP ONLY so nothing that passed can
start failing: 75 → JUDGE_MS, 137 → CAPTURE_MS, 74 → CAPTURE_LONG_MS,
9 → PTY_MS, 3 → PTY_LONG_MS. Raw >=60s literal count in the paid scope:
395 → 97, of which 51 are non-timeout noise (fixture dates, run IDs)
and 46 are enumerated justified holds (comment-carrying calibrated
budgets, poll-loop constants, utility spawn waits, and the seven
physical-ceiling 1_500_000 sites). The eval-budgets policy ratchet
keeps the residue from regrowing.

Known collapse: where an inner runner budget and its enclosing test
budget now share a tier, the old stagger is gone — an overrun surfaces
as a bun test timeout instead of a graceful runner timeout
(diagnosability trade, not a correctness one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage fill — 95 tests for six zero-coverage surfaces

- eval CLI family (eval-list/compare/summary + eval-select smoke): the
  primary interface to eval results had no tests; isolation via a fake
  gstack-slug under a mkdtemp HOME (the scripts' real resolution path —
  they do NOT honor GSTACK_EVAL_DIR; only EvalCollector does). Pinned
  current behavior: eval-list does NOT exclude _partial runs (documented
  improvement candidate)
- slop-diff (runs on every /review + quality-gate): fixture git repo +
  first-on-PATH npx stub (never downloads real slop-scan); no-diff
  early exit, missing-scanner fallback, fingerprint line-insensitivity,
  merge-base worktree scan
- bin/gstack-code-intelligence CLI arg surface (lib was covered, the
  284-line CLI wasn't): select/consent/suggest/index/search gating;
  pinned: --help routes to usage failure exit 1 (no handler)
- browse media-extract: the page.evaluate callback exercised in-process
  against a mock DOM (no exports added) — lazy-src fallback chain,
  HLS/DASH detection, bg-image url() parsing, 500-element cap
- browse session-cookie-store: factory contract (cookieName/ttlMs/
  maxSessions eviction, cross-store isolation, mint→validate
  round-trip); store is in-memory — no fs cases exist
- lib/version-source direct unit tests (gstack-version-bump.test.ts
  spawns the bin, never imports the lib): parse/format/cmp/bump
  coercion, npm 4→3 translation, #2501 mangled-JSON regression class

All hermetic (mkdtemp homes, runBin child isolation); windows curation
correctly partitions the six.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): first runBin migration batch (3 of ~36 run() duplicates)

explain-level-config, benchmark-cli, evidence move onto the shared
helper; each file's remaining special-case spawnSync sites (raw-buffer
probes, env-scrub probes) stay put deliberately. 55/55 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(evals): paid shards spool to disk + shared runShardChild lifecycle

- runPaidShard no longer buffers whole 30-min stream-json streams in
  RAM (x concurrent jobs): every byte tees to a per-shard log file
  (slug-named, path printed at START for mid-run inspection and on the
  FAILED terminal line); failures print a 64KiB tail read back from
  disk; passing shards stay quiet (the file is the record) — the free
  runner's proven contract. Classification unchanged: the strict
  classifier still sees every byte first.
- the ~35 duplicated spawn/group-kill/wall-timer/finally-reap lines
  move into runShardChild in test-strict-output.ts (detached-per-
  platform spawn, signal forwarding, SIGKILL group kill at the wall,
  drain-before-verdict); designed so the free runner can migrate later
- expectedFiles drift fixed toward ENFORCEMENT: the injected-command
  exemption is gone — a fake command exiting 0 without bun's terminal
  summary now reads FAILED (pinned: silent-pass → failed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(evals): parent-computed selection propagates to shard children

The sharded runner computed diff selection once, then each of its 48-73
children recomputed it at module load — including, on touchfiles-diff
branches, a per-child bun subprocess evaluating the old data file (20s
timeout each). The parent now serializes {version, selected, reason} as
EVALS_SELECTION_JSON into the shard env; e2e-helpers adopts it at load.
Fail-open preserved: any parse/shape violation → ONE stderr warning +
local recompute; absent env → silent local compute (non-sharded
entrypoints unchanged). Drift test pins parent→child round-trip to
identical selection decisions plus the malformed/absent cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): kill the four worst fixed sleeps (300s/30s/30s/20s)

- watchdog.test: the 20s blind wait for one production parent-watchdog
  tick becomes BROWSE_PARENT_WATCHDOG_INTERVAL_MS=250 (new env knob in
  server.ts, NaN-safe, production default unchanged) + polls for the
  boot line and the tick's stay-alive log — strictly stronger (the old
  form never proved a tick observed the parent death). 24s → 3.6s.
- stop-dead-daemon / terminal-agent-owner-watchdog: the 300s/30s
  stand-in child lifetimes become stdin-EOF-bound — the child can never
  self-exit mid-test on a slow runner (spurious-failure class) and
  self-reaps instantly if the test dies (no 300s orphans). Node-compat
  stdin APIs (owner-watchdog runs on the Windows lane).
- browser-skill-commands: the sleeper fixture's 30s self-time becomes
  8s (no stdin pipe exists in runToFiles) — far above the 1s product
  timeout it must outlive, below the test ceiling, so a timeout-kill
  regression fails on clean assertions instead of an opaque bun
  timeout; added: stdout must NOT contain 'done'.

45/45 green across the four files + server tripwires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gen-skill-docs + catalog-trim leave the serial mutator shard

gen-skill-docs.test.ts's 15 in-place generator spawns now render into
mkdtemp out-dirs (gitignored-artifact reads repointed; the handshake
scan's silent console.warn degrade became a hard assertion); its
tracked-tree reads (freshness dry-run, SKILL.md content pins) stay
reads. catalog-trim needed no change beyond the earlier main() guard —
its import is now side-effect-free (pinned by the import-purity test).
Both TREE_MUTATING entries deleted in this commit, per the transition
rule: an entry leaves in the same commit as the file's last in-place
write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): skill-validation renders codex host into an out-dir

Its 3 in-place --host codex regeneration sites collapse into one
module-level --out-dir render; assertions untouched. TREE_MUTATING
entry deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): host-config self-provisions goldens (ordering dependency severed)

Its goldens were 'produced by gen-skill-docs.test.ts' with a
when-missing beforeAll fallback that wrote the live tree — an
inter-test ordering dependency the serial shard hid. It now renders
codex+factory UNCONDITIONALLY into its own out-dir and reads goldens
only from there (the Claude golden deliberately keeps reading tracked
ship/SKILL.md — a read; out-dir claude renders repoint section-base
paths by design). TREE_MUTATING entry deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gbrain-detection-override drops mutate-then-git-restore

regenAndSnapshot renders --host claude --out-dir <mkdtemp> (+
--respect-detection) and snapshots probes from the out-dir. The
git-restore machinery is deleted outright — it restored only
PROBE_FILES of the 71 files each call wrote, so a stale tree kept the
other 68 dirty (the partial-restore bug), and its 'no output-path arg'
comment had been false since --out-dir landed. TREE_MUTATING entry
deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): catalog-mode-full renders to out-dir; restore machinery deleted

The full-catalog smoke no longer rewrites all 71 SKILL.md then
regenerates to restore (with its 'CRITICAL: failed to restore' prayer
path) — it renders into a mkdtemp and additionally asserts tracked
ship/SKILL.md is byte-unchanged. TREE_MUTATING entry deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): idempotency proof strengthens to two-out-dir recursive diff

Two renders into two separate out-dirs, EVERY file diffed byte-for-byte
(claude-only and --host all; normalization only for each dir's own
sanctioned section-base repoint; presence-sanity lists guard against a
vacuous empty-dir pass) — strictly stronger than the old in-place
double-regen that sampled 5 files. TREE_MUTATING entry deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): spec-template-sync compares an out-dir render, not an in-place one

TREE_MUTATING entry deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): the serial tree-mutating shard dissolves — TREE_MUTATING is empty

Zero mutators remain (all eight render into out-dirs now), so the four
ratchet READERS (parity caps, size budgets, carve parity/ordering) get
a quiet tree by construction in any shard and rejoin the parallel
phase. The ~35-40s serial tail on every full-suite run is gone. The
mechanism stays: a future test that genuinely must write shared
artifacts in place earns an entry with a reason and is serialized
again; the census pin still fails on renamed keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(gen): out-dir byte-identity + tree-clean pins for external hosts

codex render: porcelain unchanged AND out-dir gstack-ship/SKILL.md
byte-identical to a fresh in-place render (+openai.yaml presence);
--host all render: exit 0, porcelain unchanged, claude + .agents +
.factory + llms.txt + openclaw docs all present in the out-dir.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): commit the initial free-test durations seed (496 files)

Recorded via --record-durations on a quiescent tree: 479s serial
total, p50 92ms / p90 1.8s / max 31.4s — the top-heavy cost shape LPT
packing exists for. A hint, not a contract: refresh opportunistically
with bun run test:free --record-durations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(evals): planner/executor/report modes — the CI re-platform surface

One PLANNER computes diff selection + the slice plan ONCE and writes a
manifest (--emit-plan <path> --slices K); K executors consume it
(--plan <path> --slice i), never self-selecting, and write slice-result
artifacts; a REPORT reconciles results against the manifest (--report
<dir>) fail-closed: a slice whose artifact never landed is a FAILURE,
a planned shard nobody reported fails, wrong-slice/duplicate/cross-tier
results fail. Kills per-slice selector divergence and hollow-lane
aggregation at the root.

- hollow-shard guard: under EVALS_ALL, exit 0 with ZERO executed tests
  (bun's 'Ran N tests' now captured by the classifier — additive) is
  'passed-empty' and fails the run; selective runs keep it 'passed'
  with one warning (in-file diff/tier self-skips are legitimate there);
  unknown counts are never guessed hollow
- retry parity: --retry 1 default + RETRY_OVERRIDES literals for the
  three files whose old matrix rows earned retries: 2 (stale entries
  pinned against disk)
- live smoke: gate plan = 48 shards across 6 slices; report mode exits
  1 on a fabricated missing slice, 0 when complete

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): sliced paid lane (planner -> 6 executors -> fail-closed report)

The parity-phase re-platform: evals.yml gains a second, sliced lane
driven by scripts/test-paid-shards.ts — the SAME engine local
eval:bg:gate uses, so CI and local share one selection engine.

- plan-slices: ONE planner (fetch-depth 0 — the only job needing
  history) emits the manifest; selection fails open to run-all, never
  per-slice (the divergence class is structurally dead)
- eval-slices: 6-way matrix consuming the manifest; PTY seed +
  skill-registration steps run unconditionally (idempotent — a sliced
  lane cannot key them on suite names); aggregate spawn budget
  6 x EVALS_JOBS=2 x EVALS_CONCURRENCY=2 = 24 lane-wide (the matrix's
  40-way per row queued session startup behind 39 siblings — the
  timeout-flake family root); slice results + spooled shard logs
  uploaded as artifacts
- slices-report: reconciles slice artifacts against the manifest
  FAIL-CLOSED via --report — a slice whose artifact never landed, or a
  planned shard nobody reported, is a failure, not an absence
- sequenced needs: evals so provider concurrency never doubles while
  both lanes coexist; the matrix + its ratchets are deleted after
  demonstrated parity (intersection + expected-additions comparison)
- workflow_dispatch gains evals_all (default true) for parity runs and
  post-merge smokes — a dispatch can never silently select zero

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): weekly periodic lane runs EVERY periodic test + gate census backstop

evals-periodic.yml re-platforms onto the sharded runner: planner
manifest → 6 executor slices → FAIL-CLOSED report. This IS the coverage
contract: all ~70 periodic-tier files weekly (EVALS_ALL=1), killing the
silent-rot class where a hard-coded 9-file matrix left ~57 files
running NOWHERE (the autoplan E2E rotted invisibly for months).

- test/helpers/periodic-exclude-data.ts: reasoned exclusions in their
  OWN literals file (deliberately not touchfiles-data — map-diff
  evaluates old versions of that file standalone). Every entry carries
  reason + tracking with a re-entry condition; the runner surfaces each
  exclusion per run; policy test pins real-file + non-empty fields.
  Initial: ship-idempotency + brain-privacy-gate (documented-red,
  never green) and skill-e2e-ios (manual hardware). The TODOS 'sidebar
  E2E trio' turned out already deleted — only tombstone tests remain.
- gate-census job: weekly EVALS_ALL gate-tier run — PR lanes are
  diff-billed, so without this the full gate census might never execute
  anywhere; with the hollow-shard guard it is a census-health check
  (exit 0 + zero executed tests fails), not just a test run.
- failure notification is a concrete gh issue UPSERT (one tracking
  issue, commented per red week — never issue-per-week spam), with
  issues:write scoped to the report job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: TESTING_INTERNALS covers the 2026-08 runner overhaul

LPT-packed free suite + --record-durations, the emptied TREE_MUTATING
mechanism, the sharded paid runner as the single selection engine,
CI planner/executor/report with the fail-closed report and hollow-shard
guard, the weekly coverage contract + exclusions policy, and the
eval-budgets timeout tiers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(CLAUDE.md): testing prose matches the overhauled runners

- bun run test: duration-packed shards + --record-durations; the
  trailing serial tree-mutating shard no longer exists
- two-tier system: the sliced CI lanes (one engine local+CI), the
  weekly all-periodic coverage contract + exclusions, the gate census
- periodic detach timeout 32400 → 37800

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(TODOS): close the absorbed test-infra items, file the overhaul follow-ups

Closed with receipts: the periodic coverage contract (implemented as
full weekly coverage + exclusions), the eval-harness observability P1
(verified already landed: heartbeat, incremental _partial persistence,
live stderr + eval-watch), and the sidebar trio (already deleted —
tombstones remain). Filed: matrix deletion after parity, the
required-check maintainer decision, browse /tmp-namespace hardening,
PTY boot-readiness waits, the single typed test registry, bun-native
LPT swap, runBin/free-runner migrations, eval-list partial exclusion,
phantom key cleanup, duration-weighted slicing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v1.73.0.0: test/CI overhaul — green means green, suites restructured for speed

Version + release notes for the audit-and-overhaul branch: every
silently-skipping or never-running test class fixed and tripwired, the
free suite duration-packed with the serial mutator shard dissolved, the
paid lane re-platformed onto the sharded runner (planner/slices/
fail-closed report, parity phase), the weekly all-periodic coverage
contract, eval-budget timeout tiers, and 95 new coverage tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): first-live-run fixes — executor history + two environment-blind assertions

The sliced lane's first run (PR #2721) did its job: the planner and
report worked, the manifest governed, and every failure had a name.
Three were fixable on the spot:

- executor + gate-census checkouts get fetch-depth: 0 — files with
  SELF-derived selection (the LLM-judge map, routing) walk git at
  module load, and selection is deliberately fail-closed on git errors,
  so the shallow checkout crashed those shards ('ambiguous argument
  main...HEAD'). The manifest still governs WHICH shards run.
- landscape --toc gate: the exact toBe(3) landscape-page count was
  font-metric-dependent (3 on Amazon Linux, 2 on ubuntu CI — the same
  disease the file's own page-index comment warns about). Now a
  comparative invariant: --toc must not CHANGE the landscape count vs
  a baseline render.
- paid-run-manifest parse test builds its manifest under EVALS_ALL so
  it never walks git (proven with GIT_DIR=/nonexistent).

Remaining first-run failures are newly-exposed rot in gate files that
had never executed in CI (skillify D1 refusal, session-intelligence
context-restore, one tpa-apple-ban retry flake) — being probed
separately; they are the lane WORKING, not the lane failing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(TODOS): file the three first-execution findings from the sliced lane's live run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v1.74.0.0: queue-advance — #2722 claims the v1.73.0.0 slot

The version gate caught a live queue collision (its whole job); same
MINOR bump level, next free slot per bin/gstack-next-version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): per-shard CHROMIUM_PROFILE — the collision class duration packing exposed

Nine test files launch in-process persistent contexts or daemons that
default to the SHARED ~/.gstack/chromium-profile. Two concurrent shard
processes on one profile dir kill each other's browser — observed live
on CI once duration packing recomposed shards: handoff's
launchPersistentContext died 'Target page, context or browser has been
closed' (--user-data-dir=~/.gstack/chromium-profile in the call log)
while a sibling shard's daemon logged 'Chromium process crashed'. Hash
sharding had masked the collision by chance placement; handoff passes
standalone everywhere.

Fix at the runner, not per file: each shard child gets
CHROMIUM_PROFILE=<shard-state>/chromium-profile (the documented env
knob, same isolation idea as the existing per-shard TMPDIR). Files
within a shard run serially, so sharing the per-shard profile is safe;
config.test's resolution-order tests save/restore the env around their
assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): landscape --toc gate asserts promotion PRESENCE, not counts

Two rounds of CI receipts: the exact toBe(3) was font-metric-coupled
(3 on Amazon Linux, 2 on ubuntu), and the baseline-comparison repair
then failed 2-vs-3 across renders SECONDS apart in one CI job while the
sibling no-toc test saw 3 — per-render image-promotion timing makes any
count assertion here a coin flip. The sibling test owns exact promotion
counts; this test's actual invariant is that --toc does not break the
promotion machinery: >=1 landscape page + the TOC rendered. Also drops
the second render (halves the test's runtime).

Flaky per-render image promotion itself is worth its own look — noted
in TODOS with these receipts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(TODOS): file the per-render image-promotion nondeterminism (receipts from PR #2721)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): per-FILE Chromium profiles for the nine in-process launcher files

Completes the profile-isolation work: the per-shard CHROMIUM_PROFILE
stopped cross-shard kills; these nine files launch in-process
persistent contexts and could still collide with a lingering daemon a
sibling file spawned on the SAME shard profile. Each now scopes a
mkdtemp profile via beforeAll/afterAll (the module-scope-tripwire-safe
pattern), cleaned up per file. All nine green solo and in combined
runs, except the pre-existing commands+snapshot pairing — proven
identical WITH and WITHOUT these edits (baseline receipts) — which is
the daemon-lifecycle follow-up now extended in TODOS with this
session's receipts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): Chromium-crash exit is daemon-only — embedded launches never kill their host

handleChromiumDisconnect unconditionally process.exit()ed. Correct for
the standalone daemon (its supervisor/user must notice); suicidal when
a TEST launches BrowserManager in-process: a mid-suite Chromium death
exited the whole bun shard with no terminal summary — the exact
truncation class the strict runner flags (observed live: CI shard 1 on
eb233299 died at cache-concurrent-refresh right after a daemon-spawning
gate test; with this fix the same pairing runs to completion and
REPORTS instead of dying).

The standalone entrypoint opts in via markDaemonProcess() under
server.ts's import.meta.main gate — the same embedder contract its
signal handlers already use (gbrowser phoenix keeps its own handlers).
Embedded contexts now get the disconnect log line and continue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): context-restore assertion is evidence-based, not prose-matching

The test failed twice per run in TWO CI cycles while passing locally
4/4: the prompt said 'present the content' and the check grepped the
FINAL message for exact phrases — local runs quoted the file, CI runs
paraphrased ('the most recent context is from branch-b...') and the
substring check lost the coin flip.

- prompt now demands machine-checkable output: the newest file's
  '## Working on:' heading VERBATIM + a literal 'RESTORED: <filename>'
  marker (the mtime-scramble and cross-branch subject matter untouched)
- assertion ordered strongest-first: RESTORED marker → legacy content
  phrases → tool-call corroboration (Read/Bash input naming the newer
  file, credited ONLY when the older file was never read — a
  both-files run must still present the right one)
- the older-file negative got STRONGER: an explicit RESTORED marker
  naming the older file fails even if wintermute words appear elsewhere
- sibling scan: context-recovery-artifacts got the additive prompt-side
  treatment only (quote the matched literals verbatim); its lenient
  1-of-6 assertion deliberately unchanged

3/3 consecutive local green with all evidence classes firing
(marker=true, content=true, toolNewer=true, toolOlder=false).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): skillify family — HOME==cwd broke project-skill registration

Root cause (forensically pinned from stream-json init events + a
kill-after-init probe): with HOME set EQUAL to the child's cwd, claude
resolves <cwd>/.claude/skills as the PERSONAL skills directory and the
seeded project-tier skills never register — the Skill tool returned
'Unknown skill'. The provenance-refusal test then improvised a refusal
whose wording missed the regex (the deterministic CI+local red); the
happy-path and approval-reject siblings passed only because their
agents self-recovered by Reading SKILL.md manually — silently not
exercising the Skill-tool path at all.

All three tests now use HOME=<workDir>/home (a fresh subdir keeps the
override's intent: child ~/.gstack writes land in the assertable
sandbox, without the cwd collision). Refusal test additionally: a
'not registered/unknown skill' tripwire (a not-loaded skill can never
pass as a refusal) and the refusal regex now matches assistant text
only — the skill BODY echoed into the transcript contains the exact
refusal message, so the old full-surface match could pass vacuously
once the skill loaded. Sibling disk assertions sweep both $HOME/.gstack
and cwd .gstack roots (positives and negatives).

Verified paid: refusal 2x consecutive green with the skill's EXACT
message rendered ('Launching skill: skillify' in-transcript), then the
full file 5/5 green (~$1.35) with both siblings driving real Skill
calls (25-27 turns each).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(TODOS): two of three first-execution findings fixed (skillify family, context-restore)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): context-restore gets a private home — the REAL root cause was fixture sharing

The evidence-based assertion fix was treating a symptom. The slice
artifact's embedded transcript showed the CI agent restoring
20260829-context-save-skill-test.md — the checkpoint the SIBLING
context-save test wrote into the SHARED gstackHome checkpoints dir,
which by filename-prefix ordering genuinely IS the newest. The agent
behaved CORRECTLY; the test's fixture set was open to concurrent
sibling writes, and bun --concurrent ordering differs between CI (save
finished first) and local (restore listed first) — the entire
local-green/CI-red split explained.

The restore test now uses its own .gstack-restore-home (the whole home
moves, not just the handed path — an agent deriving the dir from
GSTACK_HOME/projects/<slug> must land in the closed set too). Full file
4/4 paid green with all evidence flags firing.

Also: the on-failure shard-log artifact glob uploaded nothing — the
Fix-bun-temp step points TMPDIR at /home/runner/.cache, so the spool
lands there, not /tmp. Both eval workflows now glob both locations
(this gap is why diagnosing THIS failure required digging transcripts
out of the slice-results artifact).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(evidence): carry the real index mtime onto gstack-wtree's temp copy

The stat-cache seed (cp of the real index) stamped the temp index "now",
which defeats git's racy-git protection: an entry is only re-hashed when
its cached mtime is not older than the index file itself, so a same-size
rewrite landing in the same second as the last real index write looked
non-racy, kept its stale stat-cache entry, and vanished from the
fingerprint — evidence stayed FRESH after a source change. This is the
CI flake in test/evidence.test.ts "allow-paths carve-out" (sub-second
alignment on fast runners: expected STALE exit 1, got FRESH exit 0).

touch -r restores the original index timestamp, reinstating the exact
racy window git itself uses. Deterministic regression pin in
test/review-log.test.ts reproduces the miss with pinned zero-nsec
timestamps (fails on the old script, passes now); receipts: manual
probe shows the fresh-stamped copy returning the clean tree for a
same-size 'hello'→'howdy' rewrite while the mtime-carried copy detects
it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): landscape gate bounds the promotion count instead of pinning 3

The alt-hinted image promotion rides the per-render measurement race
already filed in TODOS (2-vs-3 landscape pages on renders seconds
apart — CI receipts from PR #2721, now reproduced locally). Pin the
two deterministic promotions as the floor and the three promotable
blocks as the ceiling (anything above 3 means the veto leaked); the
veto/portrait assertions remain exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Test <test@test.com>
2026-08-29 09:06:54 -07:00
Garry TanandClaude Fable 5 410b4928e7 v1.66.0.0 feat: test/evals/CI speedup — 90s truthful free suite, diff-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>
2026-08-15 22:20:30 -07:00
Garry TanandClaude Fable 5 c118e2402e v1.64.1.0 v1.64.1.0: the code-smell fix wave — every pipeline guard now provably fires (net −24,943 lines) (#2572)
* fix(ci): skill-docs freshness gate covers all 10 hosts and can actually fail

The Codex/Factory gates ran 'git diff --exit-code -- .agents/' / '-- .factory/',
but both paths are gitignored (.gitignore:16-17) — git diff on ignored untracked
paths is always empty, so those two gates were structurally incapable of failing
and 7 of 10 hosts had no gate at all.

New shape: one 'gen:skill-docs --host all' pass (the generator hard-fails on any
per-host error, gating all 10 hosts on generates-cleanly), byte-freshness via
git diff for tracked output, plus a porcelain check that fails on untracked
generated strays (git diff can't see brand-new files). The gitignored-hosts
byte-freshness limitation is documented in the workflow comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): exorcise the sidebar-agent ghost from the test suite

browse/src/sidebar-agent.ts was deleted in the v1.14 sidebar refactor, but the
test suite kept testing it for 48 versions. Nothing noticed because the free
suite runs in no CI job and Bun-era module-load errors were suppressed in the
Windows shard runner via an exclusion pattern whose own comment documented the
breakage ('broken on every platform since v1.14 ... exit 0').

- Delete sidebar-security.test.ts + security-source-contracts.test.ts: crashed
  at module load (unguarded readFileSync of the deleted file); per-assertion
  triage confirmed every SERVER_SRC pin targeted the deleted chat prompt
  builder (zero hits in today's server.ts) — nothing to port.
- Delete sidebar-integration.test.ts: 11 of 13 tests exercised deleted
  endpoints (/sidebar-command queue, /sidebar-agent/event, chat buffer); the 2
  passing tests pinned only the blanket auth gate, covered by
  server-auth.test.ts + dual-listener.test.ts.
- Delete test/skill-e2e-sidebar.test.ts: E2E for the deleted queue flow.
- sidebar-ux.test.ts 1,669 -> 830 lines: 20 dead-chat describes + 15 dead
  tests removed (incl. 10 vacuous passes asserting on empty indexOf slices);
  2 stale pins on LIVE features fixed (content.js typed-catch CSSOM fallback,
  arrow-hint window widened). 95 pass / 0 fail.
- sidebar-tabs.test.ts: both failures were stale pins, not regressions —
  forceRestart's deliberate ws.close(4001) and the terminal-agent spawn that
  moved into spawnTerminalAgent() (identity-based kill refactor). 28 pass.
- touchfiles.ts: drop the three sidebar E2E entries from BOTH maps
  (E2E_TOUCHFILES + E2E_TIERS) — they pointed diff-selection at the deleted
  file, so those tests were unreachable by any diff.
- test-free-shards.ts: remove the now-dead sidebar-agent exclusion pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): run the free test suite in CI (it ran nowhere)

The full free suite (bun test: browse/test/ + test/ + make-pdf/test/) had no CI
job on any Linux/macOS runner — only Windows curated shards, paid evals, and
doc-freshness gates existed. That's how two module-load-crashing test files
survived 48 versions.

Same cached Dockerfile.ci image and container wiring as evals.yml (deps
restore, build, Chromium verify). Includes a module-load-error guard: older
Bun reported test-file import crashes with exit 0 on macOS/Linux, so the job
also fails on any nonzero 'N errors' count in the summary — future crash-class
regressions can't hide from the exact job built to catch them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): validate touchfile dependency paths exist on disk

New guard in touchfiles.test.ts: every non-glob dep path must exist, and every
glob's anchor directory must exist. This is the axis the 181-key two-map sync
discipline never covered — an entry can point at a long-deleted file and
diff-based selection then silently never triggers those tests (the sidebar
trio sat rotted for 48 versions).

First run immediately caught a fourth rotted entry: 'spec authored quality'
referenced test/fixtures/spec/** (directory does not exist) and selected for a
judge test that exists nowhere in the repo. Removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): remove deleted /sidebar-chat endpoint from tunnel allowlist

TUNNEL_PATHS is the audited tunnel attack surface — its own comment says every
addition widens it. '/sidebar-chat' stayed in the set after the endpoint was
deleted with the chat-queue path, meaning any future route matching that path
would have been silently tunnel-exposed. The set is now exactly the pair
ceremony (/connect) and the scoped command endpoint (/command), and the
dual-listener closed-set pin enforces that.

Also repairs a pre-existing red pin in dual-listener.test.ts: v1.63.0.0 made
the tunnel allowlist args-aware (canDispatchOverTunnel gained a second param)
without updating the test — red on main since then, invisible because the free
suite had no CI job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): delete chain's shadow dispatcher that skipped every security gate

meta-commands.ts carried a 'CLI mode' fallback that re-implemented command
routing without the server pipeline's gates: no scope check, no domain check,
no tab ownership, no rate limit, no hidden-element stripping, no scoped-token
enveloping — and it called handleReadCommand without a BrowserManager, which
also skipped the JS-origin cookie-exfiltration assertion. It was unreachable
in production (server.ts always passes executeCommand) and one boolean away
from being live.

chain now hard-errors without a server context. handleReadCommand's bm param
is required and assertJsOriginAllowed runs unconditionally. The chain tests
that exercised the deleted fallback now route through a server-shaped
executeCommand adapter (real handlers + trust wrapping + {status,result}
envelope), so their behavioral coverage — sequencing, trust markers, pipe
format, aliases, error reporting — survives on the production-shaped path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(extension): delete the dead chat-queue client surface

The sidebar-command handler in background.js POSTed to a server endpoint that
no longer exists (deleted with the chat queue) — ~35 lines of fully-wired dead
code including error handling for the permanent 404, plus its allowlist entry.
No sender in the extension ever emitted the message type.

chatEnabled leaves the /health contract (server hardcoded false, background.js
re-derived it, nothing consumed it — the chat input element it guarded is gone
from sidepanel.html). BROWSE_SIDEBAR_CHAT env flag had zero readers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): delete dead exports the ripped chat path left behind

Three-way split by importer class:

(a) Zero importers, deleted: the whole attack-attempt logging cluster in
security.ts (logAttempt, AttemptRecord, salted hashPayload + device-salt,
attempts.jsonl rotation, telemetry spawn plumbing incl.
buildTelemetrySpawnCommand/resolveBashBinary — the LIVE attempts.jsonl writer
is tunnel-denial-log.ts with its own rotation); the decision-file handshake
(writeDecision/readDecision/clearDecision/excerptForReview — written for
sidebar-agent's poll loop, which no longer exists); sidebar-utils.ts (whole
module — its sanitizeExtensionUrl 'sanitized before embedding in a prompt'
for the deleted prompt builder); 8 dead server.ts imports (sanitizeExtensionUrl,
generateCanary, injectCanary, writeDecision, rotateRoot, serializeRegistry,
restoreRegistry, clearAgentRecord); buildPtyClearCookie + buildSseClearCookie;
WEBDRIVER_MASK_SCRIPT (orphaned by the D7 stealth narrowing — applyStealth
never used it).

(b) Dead-pin tests edited with their exports: the 'still exported' pin in
stealth-layer-c, the string-content describe in stealth-webdriver (its live
applyStealth behavioral coverage untouched), the clear-cookie assertions,
security-review-flow.test.ts deleted whole (all 4 describes exercised the
dead decision mechanism, incl. a 'simulated sidebar-agent poll loop').

(c) KEPT deliberately: leaseCount (live behavioral coverage),
extractPtyCookie + validatePtySessionToken (extractPtyCookie is adopted by
the terminal-agent cookie-parse unification later in this wave),
resetSessionMarker + clearContentFilters (test-support API for the live
content-security layer).

Also fixes two pre-existing red pins found while here, invisible until the
free suite got a CI job: the v1.44 spawnClaude->maybeSpawnPty rename in
terminal-agent.test.ts, and a cross-file test-isolation bug where
content-security.test.ts's clearContentFilters() wiped the auto-registered
url-blocklist filter for every later file in the same bun process
(security-integration.test.ts failed on co-run; afterAll now restores it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): delete the dead ML layers — transcript classifier and DeBERTa ensemble

The L4b Haiku transcript classifier and the opt-in DeBERTa ensemble
(GSTACK_SECURITY_ENSEMBLE=deberta, a documented 721MB download) had ZERO
production callers since the chat-path agent that invoked them was ripped.
The only live ML path is scanPageContent (testsavant) inside the security
sidecar subprocess. Deleted by import graph:

- security-classifier.ts 614 -> 265 lines: HAIKU_MODEL, checkTranscript,
  shouldRunTranscriptCheck, loadDeberta, scanPageContentDeberta, ToolCallInput,
  all DEBERTA_* consts + load state. Header now states the live truth
  (imported only by security-sidecar-entry.ts). downloadFile kept, name
  intact — it is an enumerated egress sink (HF model download).
- security-bunnative.ts + test: a research skeleton self-described as 'NOT a
  production replacement', shipped into src/ with zero importers.
- security-bench-ensemble{,-live}.test.ts + the Haiku response fixture: a
  paid live-model benchmark for a layer that could not fire. The
  security-classifier-tdz test's only case exercised checkTranscript — gone.
- security.ts: layer-model header rewritten to the live architecture;
  StatusDetail.layers -> {testsavant, canary}; getStatus() no longer requires
  the impossible transcript==='ok' for 'protected' (old on-disk session state
  with a transcript key is tolerated on read, never re-emitted).
- security-sidecar-entry.ts needed zero changes: it serializes
  getClassifierStatus() verbatim and no consumer read .transcript (verified
  in sidecar-client + server.ts).
- BROWSER.md security section matches reality (ensemble knob gone, 112MB not
  22MB, sidecar hosting documented). combineVerdict/THRESHOLDS retained as
  the pure, tested combiner of record — comments now flag transcript/deberta
  votes as producer-less.

Net: 26 pass in security.test.ts incl. a NEW regression test for stale-
transcript disk tolerance; egress-receipt tripwire green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: scrub the sidebar-agent ghost from comments and CLAUDE.md

20+ comments across 10 files still described the deleted sidebar-agent.ts as a
live process — including load-bearing architecture claims ('IMPORTED ONLY BY
sidebar-agent.ts', 'sidebar-agent fills this in on first prompt-injection
load', 'kill sidebar-agent' in shutdown docs) and ~60 lines of tombstone
blocks in server.ts enumerating deleted identifiers by name (a false grep
surface: searching processAgentEvent hit server.ts and looked live).

CLAUDE.md's security-stack section now documents the LIVE architecture: L1-L3
content filters + testsavant via the security sidecar subprocess; the
L4b/ensemble rows, the GSTACK_SECURITY_ENSEMBLE knob, and the 721MB DeBERTa
download are gone (deleted as dead code this wave) with an explicit
do-not-re-document note; attempts.jsonl is correctly attributed to
tunnel-denial-log.ts; the no-live-writer status of classifierStatus is stated.

Comments that survive now describe what IS, not what WAS: the promotion gate
in domain-skills.ts explains why classifier_score>0 is load-bearing given no
L4 load-time scan exists; file-permissions.ts names real sensitive files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): delete the codex-helpers shadow module

gen-skill-docs.ts imported externalSkillName (unaliased) from
resolvers/codex-helpers.ts at line 21 and then re-declared the same function
locally — the import was silently shadowed, and the imported copy was the
STALE one (it lacked the frontmatterName param the local copy grew). Three
more functions were byte-identical duplicates, imported only under _-prefixed
aliases to keep the module 'referenced', and transformFrontmatter was a
superseded hardcoded-Codex variant. Nothing else imported the module.

Also drops three dead top-of-file imports (COMMAND_DESCRIPTIONS,
SNAPSHOT_FLAGS — which pulled the whole browse/src module graph into every
generator run for nothing — and an unused review-resolver trio).

Proof: bun run gen:skill-docs exits 0 with a byte-identical tree (zero-diff
regen); gen-skill-docs.test.ts 405/405 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(server): delete ServerConfig.idleTimeoutMs + chromiumProfile — documented, never read

Both fields carried JSDoc asserting embedder behavior that did not exist:
the idle check reads the module-level IDLE_TIMEOUT_MS env constant, and both
resolveChromiumProfile() call sites pass no argument. Worse than absent — an
embedder passing idleTimeoutMs: 5000 silently got 30 minutes.

Wiring them honestly is impossible today: the idle timer, activity state, and
shutdown target are module-global, so a per-factory value would lie for any
process running more than one handler. Deleted instead, with a ServerConfig
note pointing at the deferred singleton/route-table refactor where real
support belongs. BROWSE_IDLE_TIMEOUT and CHROMIUM_PROFILE env remain the
honest knobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): wire appendSecureFile at the four real log-append sites

file-permissions.ts carries a 24-line rationale for why POSIX mode bits are
insufficient on Windows and implements appendSecureFile (0600 at create,
Windows ACL on first write only) — but its single caller was the dead
logAttempt, while the four REAL page-content log writers (console/network/
dialog logs in server.ts, the command audit log) used raw fs.appendFileSync
with no mode. Page-content-derived logs now get owner-only permissions from
birth on every platform.

Verified before wiring: mode applies atomically at create via appendFileSync
{mode}, and the ACL pass runs only on first write — no per-append subprocess
cost on the hot console-log path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stealth): handoff() uses the shared profile resolution + lock cleanup

The headless-to-headed handoff path hardcoded ~/.gstack/chromium-profile,
silently ignoring $CHROMIUM_PROFILE and $GSTACK_HOME (gbrowser's gbd sets
per-workspace profiles), and skipped cleanSingletonLocks() — so a handoff
into a profile with a stale SingletonLock could hang where launchHeaded()
would have recovered.

This was the third live drift between the three Chromium launch paths; the
first two are documented in comments as shipped stealth regressions. Minimal
targeted fix — the full buildLaunchConfig() extraction stays in the deferred
queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): resolver registry describes the template language again

Seven registered {{PLACEHOLDER}}s had zero uses in any .tmpl (checked in both
bare and :arg forms): REDACT_TAXONOMY_TABLE, TEST_COVERAGE_AUDIT_REVIEW,
MODEL_OVERLAY, QUESTION_PREFERENCE_CHECK, QUESTION_LOG, INLINE_TUNE_FEEDBACK,
MAKE_PDF_SETUP. The last two of those families are invoked programmatically by
preamble.ts (functions kept, registry entries dropped); the question-tuning
trio and the review coverage-audit wrapper were documented by their own module
as existing 'for unit testing' that no test performed — deleted, along with
generateRedactTaxonomyTable + its EXAMPLE/TIER_BLURB constants (its '/cso
renders the full table' comment was itself stale) and its test describe.

Also deletes the gated-resolver mechanism (ResolverEntry/appliesTo/
unwrapResolver + test/resolver-entry.test.ts): fully built, fully tested,
used by zero of the 65 registry entries — the generator loop simplifies to a
direct function call. CLAUDE.md's redact-doc line stops advertising the dead
token.

Proof: zero-diff regen (0 SKILL.md changed); gen-skill-docs + skill-validation
737 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): wire boundaryInstruction from host config; drop three no-op binDir ternaries

hosts/codex.ts declared boundaryInstruction and nothing read it — review.ts
kept its own byte-identical CODEX_BOUNDARY literal (verified equal + trailing
escaped newlines). The resolver now reads the config, so the boundary has one
owner. (autoplan's template carries deliberately generic variants, enforced by
gen-skill-docs.test.ts:1358 — untouched by design.)

The 'ctx.host === codex ? $GSTACK_BIN : ctx.paths.binDir' ternary appeared in
three resolvers and could never change the result: resolvers/types.ts already
sets binDir to $GSTACK_BIN for every usesEnvVars host including codex.

Proof: zero-diff regen for claude AND codex hosts; gen-skill-docs +
host-config suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test-infra): judge uses resolveClaudeBinary; eval:watch reads the real partials dir

judgePtyState spawned the bare string 'claude' three definitions below the
resolveClaudeBinary() helper this same file exports — broken under hermetic
PATHs where every other launch in the file resolves correctly.

eval:watch read _partial-e2e.json from the legacy global ~/.gstack-dev/evals/
while EvalCollector writes it into the per-project eval dir (or
GSTACK_EVAL_DIR) — so the dashboard's completed-tests panel was empty
whenever slug detection succeeded, i.e. the normal case. The heartbeat and
per-run progress logs stay global by design (session-runner.ts: 'heartbeat
stays global'). The three eval-CLI docstrings stop claiming the legacy dir
is the primary location.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): delete the superseded SDK ship-idempotency suite and three orphaned fixtures

test/skill-e2e-ship-idempotency.test.ts's own header documented that the
monolith's SDK-harness version tests a synthetic prompt while it exercises
the real /ship skill — the author knew the old suite was superseded and left
both running, two paid LLM runs for one behavior. The weaker copy is gone;
its 'ship-idempotency' diff-selection key goes with it (the dedicated file is
periodic-tier, which always runs under EVALS_ALL — the key had no remaining
consumer).

Fixture rot: test/fixtures/golden-ship-claude.md was a 128KB zero-reader
orphan that had drifted 46KB from its live successor
(test/fixtures/golden/claude-ship-SKILL.md) while looking authoritative;
parity-baseline-v1.46.0.0.json and v1.53.0.0.json had zero readers (three
tests pin three OTHER baseline versions — consolidation is queued, deletion
of the unreferenced two is free).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin): delete zero-caller scripts; make host-config-export's docstring honest

- bin/gstack-open-url (14 lines): announced in a CHANGELOG entry, wired into
  nothing, ever. bin/gstack-platform-detect (27 lines): zero callers, and its
  hand-rolled host list was already stale (SLATE_HOST.md cites it as a
  problem). Note: the deprecated gstack-brain-consumer/reader pair the audit
  flagged was already deleted upstream in v1.63 with a stay-deleted tripwire.
- scripts/task-emission-schema.ts (61 lines): a typed schema module nothing
  imported; the tasks-section comment now documents the JSONL fields inline.
- scripts/host-config-export.ts claimed to be the 'shell bridge for the bash
  setup script' — setup never calls it (its hand-rolled host lists drifting
  is a known follow-up). Docstring now states what it IS: a standalone,
  test-pinned query CLI not yet wired into setup. Its validateValue +
  CLI_REGEX/PATH_REGEX internals were dead (defined for a guarantee the
  header claimed but nothing enforced).
- KEPT deliberately: scripts/preflight-agent-sdk.ts — a documented manual
  diagnostic (CONTRIBUTING.md + USING_GBRAIN_WITH_GSTACK.md reference it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(server): one lone-surrogate sanitizer, one sanitizeReplacer, one startTunnel

Three copies of the surrogate sanitizer existed with two algorithms
(sanitize.ts regex vs a hand-rolled charCodeAt walk in server.ts — verified
byte-identical across 11 edge cases before converging) plus two identical
sanitizeReplacer definitions each wrapping a different copy. sanitize.ts is
now the single source of truth; the runs-INSIDE-JSON.stringify egress
invariant is unchanged at every call site and its pin tests were adapted to
the new import shape without losing intent.

The ngrok tunnel-start sequence existed three times in server.ts — the
/tunnel/start route and the BROWSE_TUNNEL=1 autostart were line-for-line
equivalent (a comment admitted 'Same cleanup as /tunnel/start's error path').
One startTunnel() now owns the ephemeral loopback bind, the pre-send egress
receipt, the state-file RMW via tmpStatePath(), and the ordered error-path
cleanup; callers keep their distinct response surfaces. The
BROWSE_TUNNEL_LOCAL_ONLY test path shares nothing (no ngrok, different state
field) and deliberately stays separate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): one session-cookie registry implementation, two instances

pty-session-cookie.ts and sse-session-cookie.ts were byte-identical modulo
the cookie name — mint/validate/parse/prune/TTL, the exact code a security
fix would have to land in twice (and a third hand-rolled cookie parse in
terminal-agent.ts had already diverged; unified next commit).
createSessionCookieStore() owns the implementation; both modules become thin
instantiations keeping every exported name, their distinct threat-model
docstrings, and separate token spaces (an SSE-read cookie must never grant
PTY access). pty-session-lease.ts deliberately stays out — different contract
(sessionId/secret split, refresh, env TTL).

The factory imports nothing from token-registry (cookie-picker-auth-isolation
invariant, still pinned by sse-session-cookie.test.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): terminal-agent uses the shared PTY cookie parser

The /ws upgrade's cookie fallback hand-parsed the Cookie header inline — the
fourth copy of the session-cookie parse, and the one that had already
diverged from the others. Parsing now goes through extractPtyCookie;
validation deliberately stays against the agent's own in-process validTokens
map (the server's registry lives in a different process). The ws-handler pin
test now pins the shared-parser call instead of the raw cookie-name literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(hosts): defineHost() factory — 10 copy-paste host files become declarations

hosts/*.ts were ten copies of one file: runtimeRoot byte-identical in 9/10,
pathRewrites mechanically derivable from the host name for 7/10, the 11-entry
toolRewrites map byte-identical between openclaw and gbrain, and every asset
change a 10-file edit (cursor and slate had already fallen out of three other
hand-maintained lists). defineHost() owns the defaults; each host file now
declares only what makes it different (slate/cursor: 8 lines each). Shared
constants: CROSS_MODEL_RESOLVERS, GBRAIN_RESOLVERS, EXEC_STYLE_TOOL_REWRITES.
Genuinely-different things stayed explicit: codex/factory $GSTACK_ROOT
rewrites, hermes's tool vocabulary, claude's denylist+prefixable install,
opencode's wider runtimeRoot.

Proof: JSON.stringify(ALL_HOST_CONFIGS) dump-diff before/after EMPTY (and a
runtime walk confirmed no function-valued or undefined-keyed fields, so the
JSON diff is complete); gen:skill-docs --host all zero-diff; host-config +
gen-skill-docs + idempotency suites 485/485. Host files 595 -> 285 lines.
docs/ADDING_A_HOST.md teaches the factory pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lib): fs-atomic — one atomic-write implementation, with the race actually fixed

Atomic tmp-write-then-rename was reimplemented ~20 times across lib/, bin/,
and browse/src with three tmp-suffix conventions. One of them was a latent
bug this commit closes: lib/worktree.ts used a bare '.tmp' suffix — the
deterministic-tmp collision race browse/src/server.ts documents having hit
in production (its fix, pid+random, was trapped in a comment at one site).

lib/fs-atomic.ts: atomicWriteSync (always throws, best-effort tmp cleanup,
pid+random suffix, optional mode applied at tmp creation so the file never
exists with looser permissions) + atomicWriteQuiet (shutdown paths only).
Unit tests pin the throw/quiet contracts, 0600 mode, tmp-name uniqueness
(captured via the read-only-dir failure path — Bun's fs exports are
readonly, no monkeypatching), and no-stray-tmp cleanup.

Migrated: lib/worktree.ts (the bare-.tmp bug), lib/gstack-decision.ts
(snapshot + compact log), lib/gbrain-local-status.ts (probe cache). browse
sites follow separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(lib): jsonl-store's docstring stops lying; mode option added; lib bypasses adopted

The header claimed 'single source of truth... the ONLY copy' with write-time
injection REJECTION — while appendJsonl never screened anything, only 1 of
~10 JSONL stores imported it, and a bypass appender lived in the same
directory. Now: the contract is explicit (screening is the CALLER's job via
hasInjection/firstInjectionMatch; the enforcing callers are named), a
option applies 0600 at create for sensitive stores, and the lib bypasses are
adopted (gstack-memory-helpers ×2, redact-audit-log — which keeps its chmod
backstop for files created looser by pre-mode versions). browse/src keeps
its own appenders by design (compiled-binary surface, own secure-append
helper) and the header now says so. gstack-decision's batched archive append
stays deliberate (single-write crash-window semantics appendJsonl's
one-record contract can't express).

New pins: 0600-at-create, and a test that documents appendJsonl does NOT
self-screen — so nobody can re-document it as self-screening without making
it true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): migrate hand-rolled atomic writes to lib/fs-atomic

Seven sites, each audited for its existing throw-vs-swallow contract before
migrating: writeSessionState + the four fire-and-forget tab/state writers use
atomicWriteQuiet (they swallowed before); writeAgentRecord + the boot-time
port-file write use atomicWriteSync (they threw before — and writeAgentRecord
previously leaked its tmp file on rename failure, which the helper cleans).
All carry {mode: 0o600} plus restrictFilePermissions after successful writes,
preserving the Windows ACL hardening that writeSecureFile provided (mode bits
are POSIX-only). server.ts untouched: its three state writes route through
tmpStatePath(), pinned by server-tmp-state-path.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hosts): delete five dead HostConfig fields

metadataFormat (generator hardcodes openai.yaml), sidecar (behavior lives in
setup's create_agents_sidecar — knowledge preserved as a comment in codex.ts),
install.prefixable (skill_prefix is implemented entirely in bin/gstack-config),
staticFiles (docstring cited a SOUL.md that never existed anywhere), and
adapter (its only would-be consumer, openclaw-adapter.ts, was fully dead —
with a test asserting the field was undefined). Kept: learningsMode (wired
next), linkingStrategy (validation reads it), coAuthorTrailer (consumed by
resolvers/utility.ts).

Proof: JSON dump diff shows ONLY the deleted keys vanishing; zero-diff regen
across all 10 hosts; host-config + gen-skill-docs suites green. Note: this
commit also carries chunk-23 edits to the shared hosts/claude.ts +
define-host.ts + host-config.test.ts files (skipSkills collapse, stale
line-number comment drops) — pathspec commits, concurrent prep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): preamble tiers are explicit; silent ?? 4 default becomes an error; spec stops rendering its preamble twice

Eight skills (scrape, diagram, spec, skillify, pair-agent, landing-report,
open-gstack-browser + its connect-chrome symlink) silently received the
HEAVIEST tier-4 preamble because a missing frontmatter field defaulted to 4.
Tiers are now declared in every {{PREAMBLE}} template's frontmatter and a
missing declaration throws at generation time with the template path (the 5
templates without {{PREAMBLE}} never invoke the resolver). The stale
hand-written tier-map comment (wrong in 3 of 4 rows) is gone.

Bonus bug fixed: spec/SKILL.md.tmpl mentioned {{PREAMBLE}} in prose, so the
generator inlined the ENTIRE preamble a second time — spec/SKILL.md shrinks
127,462 -> 80,924 bytes (-46,538) from de-duplication alone. skill-size-budget
gains a reasoned INTENTIONAL_SHRINKS entry (its frozen baseline had measured
the doubled-preamble bug). New tests: missing-tier throw carries the path;
every {{PREAMBLE}} template declares a tier. (Carries chunk-23 edits in the
shared test/gen-skill-docs.test.ts.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): learningsMode is read from host config, not a hardcoded host name

resolvers/learnings.ts branched on ctx.host === 'codex' while every host
declared learningsMode — the field was decorative, and the 7 hosts configured
'basic' (cursor, slate, kiro, opencode, openclaw, hermes, gbrain) silently
received the 'full' cross-project flow their runtimes can't execute (it
depends on AskUserQuestion + gstack-config plumbing). Output now matches
declaration: basic hosts get the project-scoped search block.

Blast radius proof: all committed Claude SKILL.md files and the three golden
fixtures are byte-identical; the behavior diff lands only in the gitignored
external-host trees (hand-verified: .cursor review's learnings section swaps
the cross-project AskUserQuestion block for the project-scoped search).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen): small config scrubs — openclaw blobs to real files, setup host drift, dead artifacts

- The three openclaw markdown blobs hardcoded inside gen-skill-docs.ts (which
  silently reverted any hand edit to their tracked outputs on regen) move to
  openclaw/templates/*.md source files; output shasums byte-identical.
- setup's --host allowlists gain cursor + slate — both fully registered hosts
  with generated output, but './setup --host cursor' exited 1 because two
  hand-rolled lists in setup had drifted from hosts/index.ts.
- scripts/proactive-suggestions.json deleted: 31KB regenerated on every run,
  read by nobody (the catalog-trim design's reader was never built); its
  emitter and three determinism tests (which guaranteed a file nothing reads
  didn't churn) retired with stays-retired pins.
- claude/SKILL.md.tmpl deleted: a complete 8.9KB skill that never generated
  output (directory name collides with the host id 'claude'), in no registry.
  Recoverable from git if ever wanted under a non-colliding name.
- openclaw's frozen extraFields.version '0.15.2.0' stamp dropped;
  includeSkills: [] no-ops omitted (the generator treats [] as absent);
  llms.txt 55 -> 54 skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(gen): correct preamble tiers for the 8 silently-heaviest skills

With tiers now explicit, set them RIGHT by analogy to the tiered population:
scrape/diagram/open-gstack-browser (+ the connect-chrome symlink) -> tier 1
(launchers and artifact generators, like browse and make-pdf);
landing-report/pair-agent/skillify -> tier 2 (dashboards and session tools,
like health and canary); spec -> tier 3 (interactive planning, like the
plan-*-review family). Each tier-1 skill sheds 271 lines of onboarding
prose it never needed; tier-2 shed 20 each.

Verification per the review protocol: regen diff reviewed (pure
section-removal), skill-validation + size-budget + catalog-budget +
v0-dormancy suites green (822 tests), and live smoke of the tier-corrected
skills confirms the preamble renders the intended sections at each tier.
These skills have ~no eval coverage — stated honestly; the wave's gate-tier
eval run is the backstop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(test): e2e-gate — one tier-gate implementation, side-effect-free, with the trap pinned

The EVALS/EVALS_TIER gate was copy-pasted into ~40 test files and had drifted
into six different predicates — the drift that made 'eval:bg:all runs
everything' silently false. test/helpers/e2e-gate.ts owns the semantics now:
describeE2ETier(tier) + e2eTierEnabled(tier), env read at call time, zero
side effects (the existing e2e-helpers module runs a ~30s claude ping at
import under EVALS=1, so the gate lives in its own module; purity is pinned
by tests that scan imports and comment-stripped source).

The unit matrix pins all four env combos — including EVALS=1 with EVALS_TIER
unset -> SKIP, the exact trap that made eval:bg:all a non-run. The
tier-alignment tripwire gains a second regex for the helper shape (old shape
still detected — stragglers can't hide), and the sharded paid runner's
PRE-SPAWN tier classifier learns the helper shape too: without that, every
gate-sharded run would have spawned all 28 periodic shards just to skip them,
each paying the e2e-helpers import ping (~15 min of dead wall clock in the
CI-blocking lane). Verified: gate runs exclude the 29 periodic files,
periodic excludes the 8 gate files — identical to pre-migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): migrate the 36 tier-gated eval files to describeE2ETier

Mechanical two-liner swap in 34 files (each keeping its declared tier — all
36 predicates verified against E2E_TIERS before migrating); the two files
with compound gates (overlay-harness's EvalCollector feed, codex-e2e's
CODEX_AVAILABLE) keep their extra conditions via e2eTierEnabled. Tier
rationale comments preserved. codex-e2e/gemini-e2e/benchmark-providers keep
their distinct stderr-message gate shapes by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): skill-e2e + skill-llm-eval adopt the shared selection machinery

Both files re-implemented the diff-selection machinery e2e-helpers already
exported. The helper gained computeDiffSelection() (extracted, identical
behavior) and a trailing optional selection param on the *IfSelected helpers
(defaults preserve all 30+ existing importers). skill-e2e.test.ts drops ~120
duplicated lines; skill-llm-eval keeps its LLM_JUDGE_TOUCHFILES selection and
test.concurrent semantics via testConcurrentIfSelected.

Deliberate deltas, stated: skill-e2e.test.ts now honors the EVALS_TIER
intersection its local copy lacked (affects only direct bun test invocations
of that file — it matches no eval-script glob); its recordE2E gains the
helper's three diagnostic fields; skill-llm-eval sharded solo now runs
e2e-helpers' module-scope preflight it already ran in combined processes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): kill the silent-truncation race; exempt the tier-corrected shrinks

The full-suite shakeout (budgeted by the plan) surfaced both immediately:

1. server-embedder-terminal-port.test.ts stubbed process.exit and restored
   the REAL exit in its finally — but shutdown() schedules async work that
   can call process.exit AFTER restoration, killing the entire bun process
   mid-suite with exit 0 and NO summary. This is the silent-truncation class
   the new free-suite CI job guards against, reproduced locally on the first
   full run. Exit now stays a logging no-op between tests (late async exits
   become visible stderr lines, not process death); the true exit returns in
   afterAll.

2. The 80%-of-baseline shrink guard correctly flagged the six tier-corrected
   skills — their baseline was measured at the silent tier-4 default. Added
   to INTENTIONAL_SHRINKS with the reason, joining spec's double-preamble
   entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* release: v1.64.0.0 — the code-smell fix wave

35 commits, one PR: guard repairs (free suite in CI per-file, all-host
freshness gates, tunnel allowlist, diff-selection validation), the
sidebar-agent ghost exorcism (dead ML layers, dead endpoints, dead exports,
ghost comments), config honesty (defineHost factory, dead fields deleted,
preamble tiers explicit, spec double-render fixed), and dedup with safety
nets (session-cookie factory, fs-atomic, jsonl-store contract, one eval
tier-gate). Net -24,943 lines across 183 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): free-tests step runs under bash (container sh rejects pipefail)

Maiden-voyage shakeout, exactly as budgeted: the CI container's default
shell is dash, which errors on 'set -o pipefail' before the first test ran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): free-tests curates 8 container-incompatible files with reasons

Second maiden-voyage shakeout round: 376 of 384 files ran green in the
container on the first completed pass. The 8 that can't run there yet are
excluded the same way the Windows shards curate POSIX-bound files — each
with its reason inline (headed-Chrome handoff, real-PTY round-trip, X server
management, extension-origin identity, the job's own TMPDIR override, and
three pre-existing env failures that fail on dev machines too). Anything
outside the list that fails still fails the job; trimming the list is
tracked follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gstack-config-key-locale — suppress the skill_prefix auto-relink side effect

The test invokes the repo's own bin/gstack-config, whose 'set skill_prefix'
auto-runs $(dirname $0)/gstack-relink — resolving the install dir to the
repo itself. In any environment where the loop shares a working tree (the
free-tests CI container, a fresh-HOME run), gstack-patch-names rewrote all
52 tracked SKILL.md names to gstack- prefixed, poisoning five unrelated
suites downstream (hermetic-skills-seeding, host-config golden, skill-census,
skill-validation, spec-template-sync). GSTACK_SETUP_RUNNING=1 is the
documented suppression; relink behavior stays covered by relink.test.ts's
mock install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin): gstack-codex-session-import — empty sessions dir exits 0 on Linux

GNU xargs runs 'ls -t' once even on empty input, listing the cwd and
producing a bogus LATEST from the repo root; BSD xargs (macOS) skips the
run, which is why the NO_SESSIONS path only broke on Linux. xargs -r pins
the BSD behavior on both platforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(parity): rebaseline v1.57.7.0 → v1.64.1.0 + skeleton-cap headroom

The two parallel v1.64 waves (code-smell fix wave + main's #2571) each
added shared-preamble prose, pushing document-release / design-consultation
/ cso past their size ratios on the v1.57.7.0 anchor and four carved
skeletons (plan-ceo-review, plan-eng-review, office-hours,
design-consultation) 22-280 B over their absolute caps. New baseline is
union-normalized (skeleton + sections/*.md, matching what the harness
measures); caps get +~1 KB headroom each with per-cap rationale. The
v1.57.7.0 fixture stays in test/fixtures/ for the audit trail, and
capture-parity-baseline.ts now documents the union-normalization step so
the next rebaseline doesn't re-trip on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): free-tests container parity — tools, pinned bun, git identity, mutation tripwire

- Dockerfile.ci: add python3 (gstack-jsonl-merge/brain-sync/detach shell out
  to it), file (skill-validation's binary check), poppler-utils (make-pdf
  e2e gates hard-require pdftotext/pdffonts/pdfinfo), fonts-noto-color-emoji
  (emoji render gate, mirrors make-pdf-gate.yml). Fix the bun pin: the
  bun.sh installer ignores a BUN_VERSION env var, so the old form silently
  installed latest on every rebuild (observed 1.3.13/1.3.14 drift vs the
  1.3.10 devs run locally); pass the version as the positional arg.
- free-tests.yml: git identity + safe.directory for the git-exercising
  tests (container checkout is owned by a different uid than runner);
  post-loop tree-mutation tripwire that names a tracked-file-mutating test
  instead of letting downstream collateral confuse the report; skip the
  documented variants-retry-after timing flake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bin): gstack-session-update — detached updater owns its stdio (SIGPIPE)

The backgrounded update subshell inherited the session hook's stdout/stderr
pipes. Once the hook exits and the caller closes them, any child that writes
— git pull's autostash notice, setup output — dies of SIGPIPE, logged as
PULL_FAILED exit=141 with an empty stderr capture (observed in the free-tests
container, and reachable by any production hook runner that closes stdio
promptly). Redirect the fork to /dev/null; all observability already flows
through the session-update log file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gstack-decision-bins — explicit branch context for the scope filter

CI checks out a detached HEAD, where gitBranch() returns undefined on both
the log and search sides, so an implicitly branch-scoped decision can never
surface (filterByScope requires a matching non-empty ctx.branch). Pass the
branch explicitly on both sides — the filter logic is what's under test, not
git branch detection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): ring-buffer lease interplay — same TTL window, not same millisecond

Two back-to-back mintLease() calls each stamp Date.now() + TTL; when they
straddle a millisecond boundary the exact-equality assertion flakes
(observed in CI: expiries of ...525 vs ...526). Assert the expiries are
within a 50 ms window instead — the invariant under test is that leases
share a TTL policy, not that they mint in the same clock tick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:37:04 -07:00
Garry TanandClaude Fable 5 008dd65b1f v1.64.0.0 fix wave: full tracker audit — 90 fixes, 52 issues closed, ~50 community PRs absorbed (#2571)
* fix(hooks): nest freeze/careful permissionDecision under hookSpecificOutput

Claude Code ignores a top-level permissionDecision, so the /freeze deny and
/careful ask guards silently allowed everything. Nest both under
hookSpecificOutput with permissionDecisionReason, update the shape-blind
tests to pin the nested form, and document the constraint in both skill
templates (regen included).

Closes half of #1459 (freeze enforcement chain).

Contributed by @jawadakram20 (PR #2331; team-init hunk deferred to the
dedicated team-init fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(team-init): required-mode hook blocks with nested schema + exit 2

The generated check-gstack.sh emitted a flat permissionDecision payload and
exited 0, which Claude Code ignores — required mode enforced nothing. The
generated hook now nests the deny under hookSpecificOutput and exits 2 so
the block holds even if the JSON schema drifts again. Adds a temp-repo
regression test that runs the generated hook under both installed and
missing-gstack homes.

Fixes #2413, #2296.

Contributed by @Masashi-Ono0611 (PR #2423).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(careful): close three check-careful bypasses via real JSON extraction

The grep-based command extractor stopped at the first escaped quote, so any
quoted argument truncated the command before the pattern checks ran —
`git commit -m "wip" && rm -rf /` was silently allowed. Replace it with a
python3/node JSON parse that fails CLOSED on unreadable payloads, add an
IFS/base64-to-shell obfuscation tripwire, and stop multi-line commands from
riding the single-line safe-exception whitelist (line-based grep would have
approved `rm -rf /` when a later line matched node_modules — a hazard the
real newline decoding exposed).

Contributed by @wtamminga (PR #2426; the -R hunk was dropped — it landed in
v1.61.0.0 — and output shapes updated to the nested hookSpecificOutput form).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review,autoplan): require explicit run_in_background: false on specialist agents

Claude Code v2.1.198 made subagents run in the background by default, which
inverted the old "do not use the flag" guidance: review-army specialists and
autoplan dual voices silently launched in the background and the merge step
could proceed before they completed — regressing the #497 fix. The generated
guidance now instructs an explicit run_in_background: false, and a static
tripwire fails the free suite if the inert inverted phrasing ever returns to
any generated SKILL.md.

Fixes #2440.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(investigate): anchor the scope-lock freeze hook on $HOME, not CLAUDE_SKILL_DIR

The investigate skill's PreToolUse hooks and Scope Lock probe resolved
check-freeze.sh via ${CLAUDE_SKILL_DIR}, which does not exist when
frontmatter hooks run — the || exit 0 tail then failed open, so the debug
scope boundary silently never engaged (#1871 follow-up). Anchor all four
sites on $HOME/.claude/skills/gstack/ like careful/freeze, and add a static
test asserting no frontmatter command: line in the guard-family skills ever
references CLAUDE_SKILL_DIR again.

Fixes #2469; closes the last live half of #1459 together with the
freeze/careful hookSpecificOutput fix. The broader portable-install-root
rewrite stays #1882 (its own focused PR per the TODOS.md decision).

Reported with a fix by @maxpetrusenkoagent (PR #1873; absorbed narrowly —
the cwd-walk rewrite belongs to #1882).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact): scan large diffs in line-aligned slices; stop digit-UUIDs matching as cards/phones

The prepush guard blocked any push whose added lines exceeded the engine's
1 MiB cap with engine.input_too_large — a size error naming no credential —
which trains people onto GSTACK_REDACT_PREPUSH=skip. Scan in 768 KiB
line-aligned slices instead (no pattern is multi-line, so a boundary cannot
bisect a secret); a single oversized line still goes to the engine intact and
fails closed. Also suppress card/phone matches whose span sits ENTIRELY
inside a UUID — digit-only UUID fixtures were 14 of 21 MEDIUM findings on an
ordinary branch, the noise level that stops people reading MEDIUM at all.

Fixes #2304.

Contributed by @luckywenapere (PR #2543).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(redact): block Google OAuth client secrets and Telegram bot tokens at HIGH

GOCSPX-prefixed client secrets and <bot_id>:<35-char> Telegram tokens are
never-publishable credential shapes with unambiguous formats — both now
block at HIGH like the other live-format credentials.

Contributed by @francis-eye (PR #2357).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact-prepush): resolve the real push base instead of EMPTY_TREE whole-repo scans

When the remote default branch is not main/master (or origin/HEAD is unset),
the merge-base guess failed and the hook fell back to scanning the ENTIRE
repository as added lines — re-attributing long-pushed secrets to the
current push and, on any real repo, tripping the engine byte cap so the push
blocked having scanned nothing. Derive the base from commits reachable from
no remote-tracking branch, keep the empty-tree path only for genuinely fresh
repos, and split the block message so an unscannable diff is reported as
"could not scan (fail closed)" rather than "credential found — rotate it".

Contributed by @stormeoio (PR #2398).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact-prepush): preserve the trailing newline handed to chained pre-push.local

The chaining wrapper captured stdin with $(cat), which strips the trailing
newline — a chained shell hook built on `while read` then never entered its
loop for the final (usually only) ref line and exited 0, failing OPEN. Use
the printf-x sentinel so the byte-exact input reaches the chained hook, with
tests covering both the pass-through and the short-circuit paths.

Contributed by @francis-eye (PR #2358).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact-prepush): close the ext-diff, header-lookalike, and ref-parse bypasses

Three ways the pushed diff escaped scanning: (1) a user-level diff.external
or textconv driver replaced the diff with its own output — zero '+' lines,
so the scan saw nothing (now --no-ext-diff --no-textconv); (2) an added
content line whose text begins with "++" renders as "+++…" and the blanket
header skip dropped it (now hunk-aware header detection); (3) a pre-push
ref line that failed to parse was silently skipped, leaving that ref
unscanned (now fails closed with the offending line named).

Minimal reimplementation of the two confirmed bypasses from PR #2498 by
@lubosxyz (the full PR overlaps the chunked-scan work absorbed separately),
plus the unparseable-ref hardening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pair-agent): keep the ngrok authtoken out of the transcript and shell argv

The not-authed flow told the user to paste their ngrok authtoken into the
chat so the agent could run `ngrok config add-authtoken` — putting a live
credential in the transcript, tool-call argv, and anything the transcript
syncs to. The user now runs the auth command in their own terminal; the
agent only verifies via `ngrok config check`, and a pasted token triggers a
rotate-and-reauth instruction. A static test pins that no agent-run bash
fence ever contains add-authtoken again.

Fixes #2335.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(update-check): crash emits CHECK_FAILED instead of reading as up-to-date

gstack-update-check signals "up to date" with SILENCE, and it runs under
set -e — so any unguarded mid-script failure exited quietly and was
indistinguishable from a current install. Observed live as a 45-release
silent-staleness incident. An ERR trap (with -E so it propagates into
functions) now emits a CHECK_FAILED sentinel naming the line and status,
and exits 0 so caller `|| true` guards can't eat it. Behavioral tests cover
both the crash and the healthy-silent paths; egress-receipt wiring is
untouched and still pinned by test/egress-receipt-wiring.test.ts.

Fixes #1974. (#2378's HEAD-SHA staleness half was already fixed on main by
the ls-remote + SHA-pinned VERSION resolution — close as already-fixed.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deps): bump diff 7.0.0 → 9.0.0 (GHSA-73rr-hh4g-fpgx parsePatch DoS)

The advisory affects diff 6.x–8.0.2. The only API this repo uses is
Diff.diffLines (browse/src/snapshot.ts:571, browse/src/meta-commands.ts:728),
which is unchanged across the major hop; snapshot tests pass against 9.0.0.

Closes #1588.

Contributed by @genisis0x (PR #1599; VERSION collateral stripped, lockfile
regenerated fresh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(evals): skip eval jobs deterministically on fork PRs

Fork PRs never receive repository secrets, so every API-calling eval failed
at SDK auth — but only when Docker-cache luck let the jobs start at all,
making fork PRs randomly red or grey. Skip the eval and report jobs
explicitly for fork-origin PRs, keep the image BUILD (validates
Dockerfile.ci changes) without the push a fork token can't perform, and
leave full coverage for same-repo PRs, pushes, and dispatches.

Contributed by @andrey-esipov (PR #2345).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(extension): deny token/port reads to content-script and foreign senders

background.js answered getPort — port, connected state, AND the browse
server auth token — to any sender that passed the type allowlist,
including content scripts running in web-page context and, behind only
the sender.id check, anything without extension-page provenance. The
getToken sender.tab restriction covered getToken alone, and only after
getPort had already handed out the token.

Single decision point now: extension/sender-auth.js classifies each
message type; the eight privileged types (getPort, setPort, getServerUrl,
getToken, fetchRefs, command, sidebar-command, getTabState) require an
own-extension-page sender (chrome-extension://<own id>/ URL, no
sender.tab, own sender.id). Denied senders get { error: 'unauthorized' }
and nothing else — never the token, never the port. Content-script flows
(elementPicked, pickerCancelled, inspectResult, openSidePanel) are
untouched, and the sidepanel/popup keep the getPort token field their
connect path reads. The policy mirrors the v1.63 server-side model:
AUTH_TOKEN is released only to the pinned extension Origin via
POST /extension-token, so the extension must not re-leak it to contexts
the server would never have trusted.

browse/test/extension-sender-auth.test.ts drives the real background.js
onMessage listener under a chrome stub with four sender shapes (own
extension page, own content script, foreign extension id, missing
sender.url) and pins that denied responses carry no token/port fields,
that a denied setPort never persists, that a denied command never
reaches the network, and that the inspector + tab-state flows keep
working. The helper is loaded via importScripts in the classic service
worker and require()-able from bun tests.

Contributed by @punksterlabs (PR #1822; reimplemented against the v1.63 POST /extension-token pinned-origin model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(update-check): fixture links gstack-egress-lib.sh — all 38 tests failed on main

v1.63.0.0 made bin/gstack-update-check source bin/gstack-egress-lib.sh
unconditionally, but the test fixture's GSTACK_DIR only linked gstack-config
— every test died at the source line (0/38 pass on pristine main,
verified). The suite-truncation bug hid it: the runner was killed by an
earlier file's delayed process.exit before this file ran. Link the lib like
the real install layout the script assumes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): capture active-tab state before close() — last-tab auto-create raced the close event

closeTab checked `tabId === this.activeTabId` AFTER awaiting page.close(),
but the page 'close' event handler can fire during that await and reassign
activeTabId — losing the race meant the last-tab auto-create never ran,
leaving the manager with zero tabs. Capture wasActive before closing, and
only reassign activeTabId when it no longer points at a live tab.

Part of the test-integrity repairs unmasked by the suite-truncation fix.

Contributed by @time-attack (PR #2230, browser-manager hunk).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(browse): delete the orphaned sidebar chat-queue suite; align sidebar-ux/tabs with the PTY-only sidebar

browse/test/sidebar-integration.test.ts tested the /sidebar-command queue
path ripped in v1.14 (34 references to removed endpoints — 11 permanent
failures masked by suite truncation). sidebar-ux.test.ts carried 73 failures
pinning the same dead surface (pickSidebarModel, ANALYSIS_WORDS); the trim
keeps its 108 live tests, including the background.js token/allowlist gates.
sidebar-tabs gets the two matching expectation updates.

Closes #2420, #1980.

Contributed by @time-attack (PR #2230, sidebar hunks; the
security-sidepanel-dom deletion was NOT taken — that suite pins the live
sidepanel DOM surface and passes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(browse): align dual-listener and terminal-agent static guards with the current source

Two static-grep guards pinned superseded source shapes and failed once the
suite actually ran them: the tunnel dispatch gate is args-aware since the
--out disk-write ban (canDispatchOverTunnel takes command AND args), and
lazy PTY spawn routes through the maybeSpawnPty helper since v1.44. The
updated assertions pin the current, stricter shapes (open() never spawns;
the helper is the only spawnClaude caller).

Contributed by @time-attack (PR #2230, dual-listener + terminal-agent hunks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): remove all 8 delayed process.exit teardown bombs — the tier-1 gate can finally fail

bun test runs every file in ONE process, so a 500ms setTimeout(process.exit(0))
armed in afterAll fired mid-way through a LATER file and killed the entire
suite with exit 0 and no summary — only ~16 of 434 files ran, and every
downstream failure was invisible (observed live throughout this wave's
enumeration). Changes, all guarded by fault injection:

- Replace every delayed-exit teardown with a time-boxed close of the file's
  own browser (8 files across browse/ and design/); stub the daemon
  /shutdown timer instead of letting its unconditional process.exit tear
  the runner down.
- test/no-suicide-exit.test.ts: static tripwire — no *.test.ts may schedule
  a delayed process.exit again.
- test/exit-propagation.test.ts + fixtures: fault injection with REAL bun
  output proves the truncation shape (exit 0, no summary) and that
  scripts/test-free-shards.ts now detects it: a shard exiting 0 WITHOUT
  bun's final summary line is treated as FAILED (exit code alone is not
  evidence of completion).
- handoff: the three headed-mode integration tests are darwin-skipped with
  a pointer to the known macOS headed-launch breakage (#2242/#2554); they
  keep running on Linux CI. Un-skip in the browse-daemon wave.
- feedback-roundtrip: repair the handler call sites unmasked by the fix —
  handlers take (command, args, session, bm); passing the manager where a
  session belongs broke all six tests.
- user-slug-fallback: HOME isolation makes endpoint_hash deterministic.

Fixes #2421, #2435.

Contributed by @sneakygriff (PR #2172) with repairs from @time-attack
(PR #2230 feedback-roundtrip hunks); supersedes PR #2252 by @whd4 (same
defect, credited).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: include design/test/ in the free suite and the sharded runner

design/test was absent from both the package.json test globs and TEST_ROOTS
in scripts/test-free-shards.ts — its tests (including one of the teardown
bombs removed in the previous commit) never ran in any CI or local free
run, so design fixes could ship without their unit tests executing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): reject directories when resolving the browse binary

access(X_OK) is true for directories (they carry the execute/traverse
bit on POSIX and pass the Windows existence check too), so cwd-dependent
resolution could pick the ~/.claude/skills/browse alias DIRECTORY as the
browse binary. Every browse call then exited 4 with empty stderr, which
make-pdf surfaced as "Chromium failed to launch" against a perfectly
healthy Chromium (#2156). Guard isExecutable with statSync().isFile()
so only regular files qualify.

Contributed by @jwilk-hrep (PR #2538).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): write browse-bound temp files under the safe-dirs allowlist

os.tmpdir() on macOS resolves to /var/folders/..., which fails browse's
safe-dirs validation ([/tmp, cwd]) since the v1.6.0.0 --from-file
tightening. Default PDF output (generate with no -o), the preview HTML,
tmpFile() scratch files, and setup's smoke-test fixture/output all wrote
there, so browse rejected the paths it was asked to read or write.
Export PAYLOAD_TMP_DIR from browseClient (the existing TEMP_DIR
convention: os.tmpdir() on Windows, /tmp elsewhere) and route
orchestrator.ts and setup.ts temp files through it.

Contributed by @lvthewah (PR #2505; the browse-binary directory guard
from that PR landed separately via PR #2538).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): stop URLs swallowing smartypants placeholders

A bare autolinked URL (<a href="X">X</a>) has zero whitespace between
the URL text and its own closing tag. TAG_RE carves that </a> into a
NUL-delimited SMARTPANTS_PRESERVED placeholder BEFORE the URL pass
runs, and URL_RE's \S+ swallowed the adjacent placeholder into the URL
match. The restore pass is single-shot, so the inner placeholder never
restored: raw "SMARTPANTS_PRESERVED_N" text leaked into the rendered
link, the </a> vanished, and link-blue styling bled into the rest of
the document (#2084). Excluding the NUL sentinel (\u0000) from the URL
character class stops the match from crossing into an already-carved
zone.

Contributed by @marshaung (PR #2280; PR #2339 by @BrendaB24 covered the
same smartypants defect).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): no blank first page when content precedes the first H1

Two paths put invisible content ahead of the first H1 and cost users a
blank page 1 (#1904):

- A visually-empty preamble (leading <style> block, HTML comment)
  became its own .chapter. That section took the `.chapter:first-of-type
  { break-before: auto }` exception, so the first real chapter inherited
  `break-before: page` and started on page 2. Non-rendering preambles
  now fold into the first real chapter (markup preserved, no page
  break); real text preambles keep their own chapter.
- Leading YAML frontmatter rendered as a literal paragraph of body text
  on its own first page (marked has no frontmatter awareness). It is
  now stripped before parsing; a `---` thematic break elsewhere is
  untouched.

Contributed by @jbetala7 (PR #1913).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): allow about:blank so a restarted daemon can initialise

The daemon opens its own first tab on about:blank, so blocking it in
validateNavigationUrl meant a restarted daemon could never recreate the
blank tab it starts from — and `browse newtab about:blank`, which
`make-pdf setup` runs as its Chromium smoke test, failed and surfaced
as "Chromium failed to launch" against a healthy browser.

Allow about:blank ONLY, never the about: scheme: about:blank has no
origin, loads nothing and runs nothing, while about:config and friends
are real surfaces. Exact href match (lower-cased, since the URL parser
normalises the protocol but not the opaque part), so about:blankfoo
stays blocked.

Contributed by @jwilk-hrep (PR #2537).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): drop gpt-image-2 tool model that 400s under the gpt-4o orchestrator

The Responses API rejects pairing a gpt-4o orchestrator with an
image_generation tool spec'd as model: "gpt-image-2" (400
invalid_request_error), which took every design image call offline —
generate, variants, iterate (both threaded and fresh paths), evolve,
and /design-shotgun (#1771). gpt-image-2 is only valid under a gpt-5
orchestrator; with gpt-4o the tool must omit the model field (defaults
to gpt-image-1).

Remove the model field at all five call sites and add a static-grep
tripwire test (design/test/image-gen-pairing.test.ts) that fails CI if
any design/src module reintroduces the gpt-4o + gpt-image-2 pairing.
Re-enabling gpt-image-2 later requires bumping the orchestrator off
gpt-4o in the same diff, which the tripwire permits.

Contributed by @Pablosinyores (PR #1773).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): variants AbortError message reports the real 240s timeout

generateVariant arms its abort at 240_000 ms but the AbortError branch
returned "Timeout (120s)" — off by 2x, so a user staring at the failure
could not tell whether to bump the timeout, retry, or drop the call.
Report the actual configured bound, and pin it with a test that forces
the abort path (fast-forwarding only the 240_000 ms timer) and asserts
the surfaced string matches.

Contributed by @vryahn (PR #1774).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory-ingest): stop silently ingesting 0 pages — include gitignored staging, reconcile counts

Pages stage into ~/.gstack/.staging-ingest-*/ inside a repo whose .gitignore
is `*`, and gbrain import honours .gitignore — so it collected 0 files,
imported nothing, and the ingest still reported "written: N" from the STAGED
count while advancing state, meaning no future run ever retried. Three
layers now: (1) pass --include-gitignored (root cause); (2) if the installed
gbrain predates the flag, retry without it (subcommand --help is generic, so
the attempt is the only probe) with an upgrade pointer; (3) reconcile
gbrain's imported+unchanged accounting against the staged count and REFUSE
to advance state on a shortfall, naming the gitignore collision.

Fixes #2144, #2104.

Contributed by @gawievanblerk (PR #2560) and @Charles-Grant (PR #2486).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(autoplan): task aggregator returned zero tasks on every run — jq scope bug

Inside ($commits | split("|") | ...) the "." context is the split ARRAY, so
the filter's bare .commit raised "Cannot index array with string" on every
record — and the 2>/dev/null swallowed it, so aggregation silently produced
zero tasks no matter how many the reviews emitted. Bind .commit to $c before
the pipe. Reproduced live before the fix; regenerated autoplan/SKILL.md.

Fixes #2018.

Contributed by @kkroo (PR #2416; regenerated against the current template).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(session-update): un-wedge auto-upgrade — autostash over local patches, log the pull's real reason

On a normal install the tracked files ARE locally patched (skill-prefix
name rewrites, gbrain-refresh blocks), so the bare `git pull --ff-only`
refused on every run and auto-upgrade froze forever — observed as 308
consecutive PULL_FAILED entries with the reason discarded by 2>/dev/null.
Pull now runs --autostash (local patches ride over the update and pop back),
stderr is captured into the log so a genuine failure names its cause, an
autostash pop conflict recovers to a clean tree and re-renders the patches
(gstack-patch-names + gbrain-refresh, both idempotent), and a successful
pull re-renders them as a self-heal. Behavioral tests cover the wedge shape
and the reason logging.

Fixes #2566.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: raise the free-suite per-test timeout to 30s

bun's 5s default is fine for a file run solo, but the monolithic free suite
shares one process across 100+ files whose browser instances contend for
launch slots — Playwright tests that pass in isolation time out mid-suite.
30s matches the ceiling the enumeration runs used; the sharded runner
(test:free) is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(question-log): parse native AskUserQuestion answers — every native answer logged as __unknown__

Current Claude Code returns AskUserQuestion results as an OBJECT map keyed
by question text ({answers: {question: label}}); the hook only handled the
legacy array shapes, so 86% of live records carried user_choice __unknown__
— and the bin then scored every one as followed_recommendation false,
silently poisoning plan-tune metrics. Adds the object-map extraction (exact
+ whitespace-normalized + single-question pairing, multiSelect joins,
annotations as free_text), strips the (Recommended) suffix from BOTH sides
of the comparison, skips the computation entirely on extraction failure,
and logs unrecognized shapes to hook-errors.log instead of embedding them
in the record.

Fixes #2336, #2206.

Based on the working patch in #2336 by @yijisoo; suffix comparison fix
contributed by @chuchu2781 (PR #2400).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): canonicalize slash branches to dash form — review history stops splitting

Branch-name sanitization disagreed across gstack (four incompatible rules),
so reviews for the same slash-named branch landed in multiple files and the
ship dashboard missed entries. gstack-slug now canonicalizes / to - in one
place, and ship's review lookup routes through it; goldens regenerated
against the current templates.

Fixes #1127, #2550.

Contributed by @ShuratCode (PR #2465; duplicate fixes by @xrfael-dev and
two others in PRs #1851/#1699/#1621, credited).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): resolve the project root by marker walk-up — subdirectory sessions stop misfiling state

gstack-slug derived everything from pwd, so a session in a subdirectory got
the subdir's basename as its slug (or an outer monorepo's remote), misfiling
reviews/decisions/learnings under a phantom project — and the per-pwd cache
made the wrong answer permanent. The resolver now walks up from pwd:
outermost STRONG marker wins (.git, package.json, pyproject.toml, Cargo.toml,
Gemfile, go.mod, .project.yaml), weak content markers (README, LICENSE) catch
non-code project folders, deploy artifacts are deliberately not markers, and
GSTACK_PROJECT_SLUG remains the escape hatch. The cache self-heals on
mismatch. Main-side invariants preserved on top: the unconditional
[a-zA-Z0-9._-] re-sanitize before echo and slash→dash branch canonicalization.

Fixes #1125.

Contributed by @ajeenkya (PR #1702; rebased over the sanitize and
branch-canonicalization work that landed after it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): shared spawn-bin helper — all three AskUserQuestion hooks were inert on Windows

The plan-tune hooks resolved bin scripts via new URL(import.meta.url).pathname
(which doubles the drive letter on Windows: /C:/C:/...) and spawnSync'd
extensionless bash scripts directly (unrunnable without a shell association)
— so question logging, preferences, and the error fallback all silently
no-op'd on Windows, and /plan-tune collected no data. A single spawn-bin.ts
helper now owns bin resolution (fileURLToPath) and win32 bash routing for
every hook, with static tripwires so a future hook can't reintroduce the
raw pattern. This is the one Windows-spawn idiom for hook code.

Fixes #2356.

Contributed by @rafassousa (PR #2504; supersedes PR #2399 by @chuchu2781).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(model-overlays): add fable-5, opus-4-8, and sonnet-5 overlays + resolver mappings

model-overlays/ had no entry for the current Claude generation, so every
session on a Claude 5 family or Opus 4.8 model fell through to the generic
claude.md nudges. Adds the three overlays with resolver mappings and
per-overlay tests; generated output for the default host is unchanged
(overlays activate by detected model).

Closes #2509.

Contributed by @chrisquorum (PRs #2246, #2243, #2247).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): grant icacls ACEs by *SID, not unqualified username

An unqualified username handed to icacls is ambiguous: on a machine whose
hostname equals the username (a common Windows setup), it resolves to the
MACHINE account instead of the user. Combined with /inheritance:r, that
leaves ~/.gstack with a single ACE matching nobody — the process that just
"secured" the directory locks itself out, and icacls still reports success.

Both icacls sites in the repo (restrictFilePermissions and
restrictDirectoryPermissions in browse/src/file-permissions.ts — the only
icacls call sites; setup has none) now grant via icacls' literal-SID form
`*<SID>`, resolved once per process from System32\whoami.exe (pinned to
System32 because a bare `whoami` under a bash-flavoured PATH picks up the
MSYS build, which rejects /user). Fallback when the SID can't be resolved
is the domain-qualified `USERDOMAIN\username` name, which is unambiguous
where the bare username was not.

Windows-only regression tests assert the hardened directory stays usable
by the calling process (readdir + write), which is exactly the check that
a not-toThrow assertion sailed past before.

Contributed by @asizux2 (PR #2479); the same defect was independently fixed by @Icandi40, @chiragborse1, @IntegriGit and @voltapix26.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): forward windowsHide through the bun-polyfill spawn shims

windowsHide is the one spawn option where Node's default is the opposite
of Bun's: Node shows the child's console window, Bun.spawn hides it.
The polyfill's spawn and spawnSync shims dropped the option entirely, so
the Node fallback path (dist/bun-polyfill.cjs) silently inverted the
behavior on the one platform the shim exists to serve — every watchdog
respawn of the terminal agent popped a visible bun.exe console window.

Three sites fixed:
- Bun.spawnSync shim: forwards windowsHide with Bun-matching default true
- Bun.spawn shim: same (stdio:'ignore' silences output but does NOT
  suppress the console window on Windows)
- spawnTerminalAgent in terminal-agent-control.ts: explicit
  windowsHide: true, so the Node fallback path behaves like Bun-native

An explicit windowsHide: false is honored at both shims. Three focused
tests pin the default-true, default-true-sync, and explicit-false paths
by intercepting child_process in a subprocess; the test file's require
path now uses forward slashes so it survives interpolation into a JS
string literal on Windows.

Supersedes PRs #2523, #2294 and #2290, which each covered a subset of
these sites.

Contributed by @jerrynicholsai (PR #2539); earlier fixes by @jwilk-hrep, @rroojrooj and @WimvandenHeijkant covered subsets of the same sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(watchdog): signal-0 liveness, tick-scaled respawn guard, windowsHide

Three-bug chain behind the Windows terminal-agent leak (console window
strobing every 60s, one orphaned agent per watchdog tick until the box
ran out of committable memory):

1. isProcessAlive shelled out to `tasklist /FI "PID eq <pid>"` on Windows
   with a 3s timeout. A Bun.spawnSync that hits its timeout still RETURNS
   with partial stdout, so the `.includes()` PID match read a LIVE agent
   as dead — killAgentByRecord skipped the kill, the watchdog respawned
   around the survivor, and every orphan slowed the next tasklist enough
   to produce the next false negative. Now: `process.kill(pid, 0)` on
   every platform (Node and Bun both map signal 0 to an OpenProcess
   existence check on Windows), with EPERM counted as alive. No
   subprocess, no timeout, no console window.

2. The respawn circuit-breaker was mathematically unreachable — verified
   in this tree: RESPAWN_GUARD_WINDOW_MS was a fixed 60_000 against a
   60_000ms default tick, and each tick pushes at most one respawn
   timestamp, so three pushes span ~120s and can never coexist inside a
   60s window (eviction is strict `>`, and setInterval drift plus
   per-tick work always ages the prior entry past the boundary). The
   guard could not fire at the default tick rate and a steady
   one-per-tick leak ran unbounded. The window now scales with the tick:
   max(60_000, tick * (RESPAWN_GUARD_MAX + 2)), so "3 crashes in quick
   succession → stop" holds at any tick value.

3. The tasklist probe popped a visible console per tick (no windowsHide).
   Removing the shell-out kills that site; the agent-spawn site itself
   already passes windowsHide: true (landed with the bun-polyfill
   windowsHide commit — PR #2414's terminal-agent-control.ts hunk is
   reconciled there rather than duplicated).

New browse/test/process-liveness-windows.test.ts pins all three: no
subprocess from the probe, a static tripwire against reintroducing
`tasklist` + `PID eq` liveness checks in src/, the spawnTerminalAgent
windowsHide + stdio contract, and the window-derived-from-tick
arithmetic. terminal-agent-watchdog.test.ts test 4 now pins the
window/tick relationship instead of the fixed literal that let this
ship. Also converts `new URL(import.meta.url).pathname` to
`import.meta.path` across the static-grep tests it touches — the
pathname form yields /C:/... on Windows and breaks path.resolve.

Contributed by @SYKhayyat (PR #2414).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(terminal-agent): tie agent lifetime to its owning browse server PID

The terminal agent is intentionally detached so it survives the
short-lived CLI launcher, but its real owner is the persistent browse
server. If that server crashed or was killed before running normal
shutdown, the agent was adopted by PID 1 and lived forever (#2019).

spawnTerminalAgent now requires an ownerPid and exports it to the agent
as BROWSE_OWNER_PID; all three spawn sites pass the server PID (cli.ts
cold-start, cli.ts supervisor respawn, server.ts watchdog). The agent
polls the owner with signal 0 every 15s (GSTACK_TERMINAL_OWNER_WATCHDOG_MS
to tune) on an unref'd timer and, when the owner disappears, exits
through the SAME cleanup path as an intentional SIGTERM shutdown — now
re-entrancy-guarded and also removing the terminal-internal-token file
alongside the port file and agent record.

Runtime test spawns a real agent tied to a throwaway owner process,
kills the owner, and asserts the agent exits and its discovery files
(terminal-agent-pid, terminal-port) are gone.

Reconciled with the watchdog commit's spawnTerminalAgent contract test
(process-liveness-windows.test.ts now passes ownerPid and pins the
BROWSE_OWNER_PID env forwarding).

Closes #2019.

Contributed by @csarigoz (PR #2530).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): give the bun-polyfill spawn shim a real `exited` promise

Bun.spawn exposes `proc.exited` as a Promise resolving to the exit code.
The Node fallback shim (dist/bun-polyfill.cjs) returned no such field, so
every `await proc.exited` on the Windows path resolved instantly to
undefined — the Windows cookie picker (cookie-import-browser.ts races
proc.exited at three sites) read stdout before the child produced it and
silent-failed; browser-skill-commands and terminal-agent hit the same
class.

The shim now:
- drains stdout/stderr eagerly into capped in-memory buffers (Node's
  Readables are pull-based; without draining, a child writing past the
  OS pipe buffer blocks in write() and 'exit' never fires), replaying
  them as fresh single-shot Web ReadableStreams so reads work before or
  after awaiting exit;
- caps the buffer at 16 MB (GSTACK_SPAWN_MAX_BUFFER to override), still
  draining past the cap so a runaway child can't wedge or OOM;
- resolves `exited` with Bun-matching codes (exit code, 128+signal, 1 on
  spawn error) after both pipes finish, and resolves on 'error' too —
  Node fires 'error' without 'exit' when the binary is missing, which
  otherwise hangs the await forever.

Six tests pin exit codes, the read-after-exit ordering, spawn-failure
resolution, the buffer cap, and the large-output drain. Adapted to the
current test file (require path goes through the requirePath variable
from the windowsHide commit), and the 1 MB drain test's child now exits
in the write callback — on modern Node a pipe write past the OS buffer
is async and process.exit() straight after write() truncates at ~64 KB
even with a live reader, which fails the test for reasons unrelated to
the shim.

Contributed by @punksterlabs (PR #1743).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): BROWSE_BIN carries the .exe suffix on Windows

On Windows, `bun build --compile` emits browse.exe, but setup's
BROWSE_BIN pointed at the suffixless path — so the post-build gate
(`[ ! -x "$BROWSE_BIN" ]` → "browse binary missing") could never pass on
Windows even after a fully successful build, while the build step itself
reported success. Closes #2291.

Applied the PR's override after the IS_WINDOWS detection, and also to
the second BROWSE_BIN assignment the PR predates: the direct-Codex-
install migration path re-derives BROWSE_BIN from the migrated dir and
would otherwise drop the suffix again on Windows.

Contributed by @rroojrooj (PR #1714).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): link lib/ beside bin/ at all five host-install sites

bin/ scripts import shared modules via ../lib (gstack-learnings-log →
lib/jsonl-store.ts is the reported case), so any runtime root that
exposes bin/ without lib/ breaks 13 bin/ commands — learnings-log,
decision-log, telemetry and friends fail with "Cannot find module
.../lib/jsonl-store.ts" on every non-Claude install, silently from the
skills' perspective.

All five host-install sites now carry lib/ next to bin/, each through
the existing _link_or_copy helper (never raw ln — the static invariant
in test/setup-windows-fallback.test.ts enforces this):

- .agents sidecar (create_agents_sidecar asset loop)
- Codex runtime root (create_codex_runtime_root)
- Factory runtime root (create_factory_runtime_root)
- OpenCode runtime root (create_opencode_runtime_root)
- Kiro install block

New test/setup-runtime-lib-command.test.ts executes the real setup shell
for each root in a sandbox (both the symlink branch and the Windows copy
branch of _link_or_copy) and runs gstack-learnings-log end-to-end from
the installed root, asserting the learning lands in
~/.gstack/projects/<slug>/learnings.jsonl — plus a negative control
proving a bin-without-lib root fails exactly the way the bug report did.
gen-skill-docs.test.ts's setup-validation block pins the lib link at
every site. Cross-checked against PRs #2433, #2410 and #2198: all three
cover subsets of these sites; nothing they fix is missing here.

Contributed by @fedster99 (PR #2262); overlapping fixes by @gregario, @lsendel and @netkurt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): ship supabase/config.sh with every host runtime root

Distinct from the lib/-beside-bin/ defect: gstack-telemetry-sync,
gstack-update-check, gstack-security-dashboard and
gstack-community-dashboard all source $GSTACK_DIR/supabase/config.sh to
resolve GSTACK_SUPABASE_URL, where GSTACK_DIR is the installed root
(parent of bin/). The [ -f ... ] guard means a root without the file
degrades SILENTLY — telemetry and update checks just stop resolving the
project URL on non-Claude installs. Closes #2215.

setup now links supabase/config.sh (file-level on purpose — migrations/
and functions/ are dev-only) via _link_or_copy at all five host-install
sites: the PR's four (Codex, Factory, OpenCode runtime roots + the Kiro
block) plus the .agents sidecar, whose bin/ resolves the same relative
path and which the PR predates covering.

The runtime-root test now asserts supabase/config.sh is present in
every built root, on both the symlink and Windows-copy branches.

Contributed by @jizusun (PR #2216).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(windows): curate the fix-wave regression tests into the windows-latest run

The windows-free-tests curated set is derived (POSIX-fragility regex scan
+ explicit deny list), and two of this wave's Windows regression files
were auto-excluded on false-positive pattern hits:

- browse/test/file-permissions.test.ts tripped the POSIX-mode-bitmask
  pattern, but every `mode & 0o777` assertion is platform-guarded — and
  the file carries the win32-only icacls-by-SID regression tests, which
  can only ever execute on windows-latest.
- browse/test/terminal-agent-owner-watchdog.test.ts tripped the
  spawn(['bun','run',...]) pattern whose reason is the Playwright-bound
  browse server; it actually spawns terminal-agent.ts (fs/path/crypto +
  local helpers only, no Playwright at module scope), and the owner-PID
  orphan leak it pins was reported on Windows (#2019).

Adds a KNOWN_WINDOWS_SAFE force-include list (mirror of
KNOWN_WINDOWS_INCOMPATIBLE, each entry carrying its false-positive
rationale) consulted before the pattern scan, and makes the
owner-watchdog test's throwaway owner process Windows-portable
(process.execPath instead of `sleep`, which a bare runner may not have).

The wave's other new files need no wiring: process-liveness-windows and
the bun-polyfill windowsHide/exited tests pass curation automatically;
setup-runtime-lib-command self-skips on win32 by design (its Windows
branch is exercised by simulating IS_WINDOWS=1 under bash), so
force-including it would add a permanently-skipped file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): register the SessionStart hook with a bash prefix on Windows

Windows can't execute an extensionless bash script directly — registering
the bare gstack-session-update path made the hook pop the "Select an app"
dialog on every session start (or silently never run), so team-mode
auto-upgrade was dead on Windows installs. Companion to the hooks'
spawn-bin routing: same defect class at the registration site.

Contributed by @NikhileshNanduri (PR #1813; VERSION/CHANGELOG collateral
stripped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): stop piping gen:skill-docs through tail — generator failures were masked

setup piped doc generation through `tail -3`, so a generator crash kept the
pipe's exit 0 and installs completed "successfully" with broken or missing
SKILL.md files. Capture the real exit status at BOTH sites (the main
gen:skill-docs step and the gbrain-detected gen:skill-docs:user regen —
the second drifted in after the PR and its own test caught it), print the
tail for UX, and fail loudly.

Contributed by @DavidMiserak (PR #1898; VERSION/CHANGELOG collateral
stripped; extended to the second pipe site).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mktemp): move the X-run to the end of every temp-file template (BSD/busybox safe)

BSD mktemp (macOS) does not substitute an X-run that has a suffix after it:
`mktemp "$TMP_ROOT/codex-err-XXXXXX.txt"` creates a LITERAL
codex-err-XXXXXX.txt on the first call (exit 0) and every later call fails
with `mkstemp failed: File exists` — so /codex breaks from the SECOND run on
every Mac, masquerading as a model stall. busybox mktemp (Alpine) rejects the
template on the first run. Fixes #2091, #2370.

Union of both community fixes, compared at the diff level:
- PR #2372: all 11 source sites with a suffix after the X-run — codex
  SKILL.md.tmpl (5), claude SKILL.md.tmpl (3), bin/gstack-developer-profile
  (2, suffix folded into the prefix: .json.tmp.XXXXXX), and the office-hours
  codex pass in scripts/resolvers/review.ts (1).
- PR #2103: the second half of #2091 — bin/gstack-paths now strips the
  trailing slash from TMP_ROOT at the source (macOS $TMPDIR ends in `/`),
  plus runtime tests pinning that normalization.

New repo-wide tripwire in test/regression-issue2091-bsd-mktemp.test.ts:
every .tmpl, every SKILL.md, and every scripts/resolvers/*.ts is swept —
no mktemp template may carry a suffix after the X-run, with a self-test so
the detector can't be quietly blinded. Generated SKILL.md files regenerated
via gen:skill-docs in this commit.

Contributed by @ShuratCode (PR #2103) and @noron12234 (PR #2372); PR #2285 by @cathrynlavery covered a subset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(codex,review,ship): scope codex review with an explicit --base flag, never prompt text

`codex review` takes its scope ONLY from --base/--commit/--uncommitted. The
positional [PROMPT] is mutually exclusive with all three, and a prompt-only
`codex review "<text>"` silently falls back to the uncommitted working-tree
scope (verified on 0.144.1: it runs `git status --short; git diff` and
reviews that) — so the previous prompt-based scoping produced a
confidently-worded review of the WRONG changes and read "no changes" on a
clean tree. Every diff pass now invokes `codex review --base <base>` with no
prompt argument: /codex Step 2A default path, the /review structured pass,
and the /ship adversarial-section pass (all via scripts/resolvers/review.ts).

Custom review instructions keep their own `codex exec` path (the CLI rejects
prompt + scope flag together), with the filesystem boundary preserved there.
Two new Error Handling entries teach the failure shapes: the argv-parse
error, and the "review says no changes on a branch full of changes" symptom.

Tests updated to pin the new invariant instead of banning the fix: the old
assertions required the diff range in prompt text and banned the
`--base <base> -c '...'` substring, which the correct scoped form contains.
Also deletes test/fixtures/golden-ship-claude.md — a 2,565-line orphaned
fixture referenced by zero tests (the live goldens are in
test/fixtures/golden/, compared by test/host-config.test.ts); the factory
golden is refreshed from the regenerated output. Generated SKILL.md files
regenerated via gen:skill-docs in this commit.

Contributed by @fangearhq-boop (PR #2513).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review,ship): run the codex diff passes under the timeout wrapper (#1036)

The `_gstack_codex_timeout_wrapper` added in #1056 was wired into
codex/SKILL.md but never into the /review and /ship diff passes, which kept
running under a bare 5-minute Bash gate. An unwrapped stall returns no exit
code and no output, which downstream reads as "Codex reviewed and found
nothing" — a truncated pass silently became a clean bill. Measured on
codex-cli 0.145.0: a pass was killed at 287s of a 300s budget mid-tool-call,
and the same prompt completed in 336s.

Both passes in scripts/resolvers/review.ts (adversarial `codex exec` and the
structured `codex review --base` pass) now re-source gstack-codex-probe and
run under `_gstack_codex_timeout_wrapper 540`, with the Bash tool gate raised
to 600000 ms so the wrapper fires FIRST and a stall surfaces as a diagnosable
exit 124. The timeout guidance now says a timed-out pass is MISSING COVERAGE,
not a clean result, and points at the run's rollout log under
~/.codex/sessions/ for partial output. The stale "timeout doesn't exist on
macOS" claim is gone — the wrapper resolves gtimeout, then timeout, then runs
unwrapped, so it is safe without coreutils.

Static guards in test/codex-hardening.test.ts pin all three sites (resolver,
review/SKILL.md, ship/sections/adversarial.md): both calls wrapped, wrapper
budget strictly under the Bash gate, and no reappearance of the macOS claim
that steered these call sites away from the wrapper in the first place. The
Claude-output path guard in test/gen-skill-docs.test.ts now scrubs
~/.codex/sessions/ (a user-facing Codex CLI path, same class as the
~/.codex/logs/ exemption) before banning Codex host paths. Generated files
regenerated via gen:skill-docs; factory golden refreshed.

Contributed by @aegixx (PR #2379).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(codex): sandbox the review path, fail the gate closed, order timeouts wrapper-first

Closes #2496, #2524, #2477 — three defects in the class "a guard that
reports success while doing nothing", all in codex/SKILL.md.tmpl:

(a) Review sandbox. The default `codex review` path was the only codex call
with no sandbox override, inheriting ~/.codex/config.toml's default — write
access on a trusted project — while Important Rules claimed read-only.
Top-level `codex review` has no -s/--sandbox flag (verified on 0.147.0), so
the invocation now pins `-c 'sandbox_mode="read-only"'`, the same form the
consult-resume path already uses.

(b) Fail-closed verdict gate. The old rule ("no [P1] found → PASS") could
not fail on the default path: native `codex review` output carries no
bracketed tags, and a non-zero exit, expired auth, timeout, or empty result
also contains no [P1] — all read as PASS. The gate is now an ordered,
fail-closed check: non-zero exit → FAIL; empty output → FAIL; [P0]/[P1]
(bracketed or codex's native labels) → FAIL with count; NO severity tags at
all → FAIL requiring a human read; PASS is only reachable through the
explicit tagged-advisory-only branch. [P0] is recognized as blocking, and
the review-log findings count includes it.

(c) Bash gate above the wrapper. Step 2A instructed `timeout: 300000` under
a 330s wrapper, and Challenge's 300s gate sat under a 600s wrapper — the
harness killed the call before the wrapper could emit its diagnosable
exit-124 message. Every Bash gate now sits strictly ABOVE its wrapper:
360000 over the 330s review wrapper, 660000 over the 600s challenge/consult
wrappers, with the ordering rationale stated at each site.

Also from #2477/#2524: a new Error Handling entry for the model-entitlement
400 ("The '<model>' model is not supported...") pointing at the `model =`
pin and `[notice.model_migrations]` in ~/.codex/config.toml and saying
exactly which override to retry with (-m for exec-based modes,
`-c model="..."` for review mode, which rejects -m); the Model & Reasoning
section no longer documents `-m` for `/codex review`.

Static assertions in test/codex-hardening.test.ts pin (a)-(c) across both
the .tmpl and the generated SKILL.md: every scoped review invocation carries
sandbox_mode="read-only" and never -s; the default-PASS sentence is banned
and the fail-closed branches are present; and per-section, every Bash
`timeout: N` is strictly greater than every wrapper budget, with 2A/2B/2C
all required to be inspected. Generated SKILL.md regenerated via
gen:skill-docs in this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(preamble): quoted tilde made Artifacts Sync and telemetry-finalize dead code in 49 skills

A tilde inside double quotes never expands, so the generated
`_BRAIN_SYNC_BIN="~/..."` assignments resolved to a literal ./~ path and
the Artifacts Sync + telemetry-finalize blocks silently no-op'd in every
skill that carried them (regression of #785). The preamble resolvers now
emit $HOME-based paths; all generated SKILL.md files regenerate identically
from the fixed templates, and a static tripwire fails the suite if a
quoted-tilde assignment ever reappears in generated output.

Fixes #1656, #1715.

Contributed by @jawadakram20 (PR #2333).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gen-skill-docs): stop the catalog trim chopping descriptions at embedded periods

The description-trim regex treated the first period as end-of-sentence, so
skill descriptions with embedded periods (e.g. file extensions, version
numbers) truncated mid-thought in the generated catalog — the discovery
surface every host loads. Trim now respects the full first sentence;
diagram's description regenerates to its intended text.

Contributed by @sneakygriff (PR #2171).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(preamble): update_check:false gates the prose, not just the binary

Setting update_check:false stopped the update-check BINARY from running,
but every skill preamble still shipped the upgrade-handling instruction
prose unconditionally — burning tokens on instructions that could never
fire and confusing agents into probing for upgrades anyway. The resolver
now suppresses the upgrade-flow prose when the config disables checks.

Fixes #2001.

Contributed by @jc0d35 (PR #2022).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): sidebar Terminal — drop the duplicate WS subprotocol header, stop doubling CJK IME input

The terminal client passed the auth token as the WS subprotocol AND echoed
it in a second header, which some Chromium builds reject; and composition
events double-sent CJK input (each IME commit arrived once from the
composition handler and once from the data handler). One auth path, one
input path; also fixes the terminal-agent test that failed on clean main.

Contributed by @mindsurf0176 (PR #2515).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): -h/--help prints usage instead of running the installer

Asking setup for help RAN the full installer — Playwright download and all.
Standard help flags now short-circuit to usage.

Contributed by @saen-ai (PR #1219).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hosts): Codex-generated skills reference AGENTS.md, not CLAUDE.md

Codex reads AGENTS.md, but its generated skills still told agents to read
CLAUDE.md in 8 places — instructions Codex hosts cannot follow. The host
config now maps the memory-file name per host; all three ship goldens
refreshed from the regenerated output.

Contributed by @exGeni (PR #1996).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(retro,ship): count tracked files for the test-file metric, not the working tree

The test-file count ran find over the working tree, sweeping untracked
build output — a Rails repo reported 623 test files when git tracks 17
(37x), skewing retro narratives and ship dashboards. Count via git ls-files
instead; includes the one-line Python-glob widening so non-JS repos stop
undercounting.

Fixes #2307, #1999.

Contributed by @joshRpowell (PR #2308).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(land-and-deploy,gen): auto-merge diagnosis + CRLF-stable generation

Two small hardenings: land-and-deploy Step 4 no longer misdiagnoses a
failed `gh pr merge --auto` as a permissions problem when the real cause is
the merge-method mismatch the command names; and gen-skill-docs normalizes
CRLF at the template entry point so Windows checkouts with autocrlf produce
byte-identical generated output to CI instead of silently skipping the
\n-anchored transforms.

Contributed by @Jmeg8r (PR #2437) and @1ncludeSteven (PR #1051).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(land-and-deploy): stop greedy sed from eating the URL scheme in deploy-config parsing

The deploy-config bootstrap parsed "Production URL: https://x.com" with
sed 's/.*: *//', which cuts at the LAST colon — the one in "https:" —
yielding "//x.com". Cut at the first ": " instead (s/^[^:]*: *//).

Resolver only; the generated land-and-deploy/SKILL.md regenerates from
this source in the docs lane.

Contributed by @briascoi (PRs #2555/#2493).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(artifacts-init): honor the provider CLI's git_protocol instead of forcing SSH

gstack-artifacts-init unconditionally rewrote the push remote to SSH and
hard-failed setup for users whose gh/glab auth is HTTPS-only. Now:

- provider-created remotes follow `gh config get git_protocol` /
  `glab config get git_protocol` (HTTPS when unset — the gh default)
- explicit/existing/manual remotes keep their given protocol; unknown
  URL forms (local bare paths, file://, self-hosted) pass through
- new --push-protocol auto|https|ssh flag overrides the inference
- the unreachable-remote error names the actual protocol and points at
  --push-protocol instead of assuming a missing SSH key

Closes #1348.

Contributed by @time-attack (PR #2225).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): skip the .gitignore append when git already ignores .gstack/

ensureStateDir appended ".gstack/" to a tracked .gitignore even when git
already ignored the directory via global excludes, .git/info/exclude, or a
parent .gitignore — dirtying the working tree on every daemon start. Run
`git check-ignore -q -- .gstack/` first and return early when git says it's
covered; git-missing/not-a-repo/timeout all fall through to the existing
text-check append (the safe default).

Closes #2385.

Contributed by @gregario (PR #2430).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): guard browser.process() in resolveDisconnectCause

`.process()` only exists on browsers Playwright launched itself; a browser
from connectOverCDP() (or a test stub) has no such method, so the blind call
threw "browser?.process is not a function" inside the disconnect handler and
took down the daemon. Type-check the method before calling it and treat the
no-method case as no process handle.

Closes #2085.

Contributed by @elan2002 (PR #2434).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(lib): narrow the override injection denylist to instruction-shaped phrases

The /override[:\s]/i pattern flagged any prose containing "override " or
"override:" — CLI flags (--port-override -1), tfvars notes, and plain
"you can override the default region" all tripped the injection guard.
Require an instruction-shaped continuation: "override (all)? previous |
prior | above | the rules/instructions/system prompt". Genuine attempts
like "Override: ignore all previous instructions" still block via the
ignore-previous pattern.

Closes #2401, #1934.

Contributed by @Masashi-Ono0611 (PR #2424); same fix independently by
@JonasFocus (PR #1940).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(redact): stop the E.164 phone pattern flagging compact timestamps

Bare 14-digit runs like 20260727202423 (YYYYMMDDHHMMSS backup/log stamps)
matched the phone regex and produced MEDIUM PII findings. Reject a
separator-free 14-digit span whose fields parse as a plausible date-time;
real numbers carry a + or spacing, so phone coverage is unchanged.

Contributed by @abkrim (PR #2428).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): create the OpenAI key file owner-only, closing the write-then-chmod race

saveApiKey wrote ~/.gstack/openai.json at the default umask and tightened to
0600 afterwards, leaving the API key briefly world-readable between write and
chmod (CWE-377/367). Pass mode 0o600 at create; the trailing chmodSync stays
as a backstop to tighten a pre-existing loose file.

Contributed by @bunlongheng (PR #2468).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): make gstack-config key validation locale-independent

POSIX bracket ranges like a-z follow the active collation order; under GNU
grep with tr_TR.UTF-8 the range excludes the ASCII letter i, so every key
containing i (skill_prefix, explain_level, ...) was rejected as invalid.
Pin both get/set validators to LC_ALL=C, with a source-level tripwire test
since macOS BSD grep doesn't reproduce the bug.

Closes #2494.

Contributed by @Math1987 (PR #2506).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(resolvers): stop env-var hosts from doubling $HOME in the binary fallback path

The browse/design/make-pdf setup resolvers built the fallback binary path as
"$HOME" + dir.replace(/^~/, ''), which is only correct for ~-rooted dirs.
Env-var hosts carry an absolute $GSTACK_* dir, so the generated fallback
became $HOME$GSTACK_.../browse — a path that never exists. New toShellPath()
in scripts/resolvers/types.ts expands ~ to $HOME and passes absolute
env-var dirs through untouched; all five call sites route through it.

Claude-host generated output is byte-identical, so no SKILL.md regeneration
is needed here.

Closes #2055.

Contributed by @simjak (PR #2056).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(settings-hook): respect CLAUDE_CONFIG_DIR when resolving settings.json

gstack-settings-hook hardcoded $HOME/.claude/settings.json, so users running
Claude Code with a relocated CLAUDE_CONFIG_DIR had hooks written to a config
file Claude never reads. Resolve ${CLAUDE_CONFIG_DIR:-$HOME/.claude} first;
the explicit GSTACK_SETTINGS_FILE override still wins.

Partial #349.

Contributed by @andrefogelman (PR #2239).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): dispatch a change event after fill for change-only validators

Playwright's Locator.fill() dispatches `input` but never `change`, so
frameworks that validate on change (AngularJS ng-change, debounced
strength/match checks) never saw the filled value — correct in the DOM,
failing the framework's own validation. `browse fill` now dispatches
`change` after the fill. Failing-first regression test with a
change-only password-match fixture included.

Contributed by @intelliot (PR #2475).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(safety): unknown question-preference source exits the documented 2, not 1

The --write user-origin gate documents exit 2 as "rejected, do not retry"
(profile poisoning defense), but a source outside both the allowed and the
explicitly-rejected lists fell through to exit 1 — the generic validation
code callers treat as retryable. Unknown sources now exit 2 with the same
do-not-retry rejection message as the known non-user-originated ones.

Closes #2390.

Contributed by @gregario (PR #2429).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pr-title): stop duplicating the version prefix on bare-version titles

A title that was nothing but a version ("v1.2.3" — the form ship uses for
version-only bumps) matched neither the "v<NEW_VERSION> " literal case nor
the trailing-space strip regex, fell through to the prepend path, and came
out as "v1.2.3.4 v1.2.3" — which pr-title-sync.yml then wrote back via
gh pr edit. Handle the bare form in both the no-change case and the
prefix-strip regex, and emit a bare new version when nothing follows.

Closes #1886.

Contributed by @jbetala7 (PR #1887).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(build): escape literal braces in the bun:sqlite stub regex

Perl >= 5.26 treats an unescaped literal `{` in a pattern as fatal
("Unescaped left brace in regex is illegal"), so build-node-server.sh
died at the bun:sqlite stub substitution on modern perl. Escape both
braces; the replacement output is unchanged.

Closes #2300.

Contributed by @nuga0718 (PR #2111).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): preserve spaces in gstack-config values

get/list read values with awk '{print $2}' | tr -d '[:space:]', which
truncated any value containing spaces ("/Users/x/Conductor Workspaces"
came back as "/Users/x/Conductor") and set wrote the unfiltered raw value
on the append path. New read_config_value() strips only the "key:" prefix
and trailing whitespace (cut-style parse), and set appends the same
newline-stripped value the in-place edit path uses.

Closes #1782.

Contributed by @jbetala7 (PR #1783).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): recover a late-healthy detached daemon instead of a false "Server failed to start"

startServer spawns the daemon detached + unref'd, then polls health for a
fixed budget. On a loaded machine the budget can elapse in the gap between
the loop's last tick and the daemon becoming ready — the CLI reported
"Server failed to start within Ns" while the very next `browse status`
showed a healthy server. Add a final readState()+isServerHealthy() re-check
before the timeout throw, and make the budget env-overridable via
BROWSE_START_TIMEOUT (BROWSE_* tunable convention). Structural + behavioral
tests pin both invariants.

Closes #1846.

Contributed by @harjothkhara (PR #1847).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): daemon resilience on loaded machines — Bun conn errors, stop/restart flush, startup + git-root budgets

Four load-sensitivity fixes in the daemon lifecycle:

- sendCommand only recognized Node's ECONNREFUSED/ECONNRESET; the compiled
  CLI runs on Bun, which reports 'ConnectionRefused'/'ConnectionClosed'
  ("Unable to connect..."), so daemon crashes leaked the raw error and
  exited 1 instead of entering the busy-check/restart path. Match both.
- stop/restart called shutdown() inline, which exits before the HTTP
  response flushes — the CLI saw a dropped socket (and would now
  crash-retry a fresh daemon just to stop it). Defer shutdown ~100ms so
  the 200 lands first.
- Non-CI POSIX startup budget raised 8s -> 15s (cold Chromium measured
  ~5.7s at load avg 10; load 12+ blew the old budget while the detached
  daemon was still booting).
- getGitRoot's 2s git rev-parse timeout returned null under load (6.3s
  spikes measured), scattering state files across cwds into split-brain
  daemons. Raise to 8s, still bounded.

Contributed by @mplatts (PR #1732).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): ingest keeps error_message/failed_step instead of dropping them

The telemetry_events columns exist and bin/gstack-telemetry-log already
sends error_message + failed_step, but the Supabase ingest function dropped
both fields on insert — every error report arrived with no message and no
failing step. Map them through with the same bounded-length sanitization as
error_class (500/100 chars). The completion-status resolver now also passes
--error-message/--failed-step in the generated skill telemetry block, with
instructions to leave them empty on success.

Resolver only for the template side; generated SKILL.md files regenerate
from this source in the docs lane.

Contributed by @sunnnybala (PR #769).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(browse): surface non-EEXIST errors in acquireServerLock instead of masking them

acquireServerLock caught every open failure as if the lock were held:
EACCES/EROFS/ENOENT surfaced as phantom "another process holds the lock"
(null return, no diagnostics), and a failed stale-lock read or unlink was
swallowed the same way. Each failure class now logs a coded, pathed
diagnostic: non-EEXIST open errors, holder-PID read errors (ENOENT retries
the acquire — the holder released between open and read), and stale-lock
unlink errors. Four-case unit test included.

Closes #1084.

Contributed by @jbetala7 (PR #1725); same fix independently by
@JiayuuWang (PR #1097).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(paths): shell-quote gstack-paths output so eval round-trips values

gstack-paths emitted bare KEY=VALUE lines, so the documented
eval "$(gstack-paths)" re-parsed the values: backslashes were eaten as
escapes (Windows $TMP C:\Users\... became C:Users...) and a space
word-split the assignment, leaving the variable empty. Emit each value
with printf %q so eval round-trips byte-for-byte; plain POSIX paths are
unchanged. Round-trip regression tests cover backslashes, spaces, and
embedded quotes.

Closes #2374.

Contributed by @fangearhq-boop (PR #2376); same fix independently by
@yannickspiess (PR #1580).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* security(browse): drop .svg from the load-html extension allowlist

SVG is a script-capable format (inline <script>, event handlers, foreign
objects), so allowing it through load-html's HTML allowlist let a local
.svg execute script in the browse session context. The allowlist is now
.html/.htm/.xhtml only; regression test asserts .svg is rejected.

Contributed by @garagon (PR #1153).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(benchmark): validate --timeout-ms as a positive integer

gstack-model-benchmark fed --timeout-ms straight through parseInt, so
"abc" became NaN and "0"/"-1" passed through — a NaN or non-positive
timeout silently disables the per-provider watchdog. Reject anything
that isn't a positive (optionally +-prefixed) safe integer with a clear
error and exit 1.

Closes #1726.

Contributed by @jbetala7 (PR #1727).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(fixtures): clean terminology in the security-bench replay fixture

Two spots in browse/test/fixtures/security-bench-haiku-responses.json
referred to real-world HVAC project naming; replace with the generic
"mechanical services" wording. Fixture stays valid JSON; replay tests
unchanged.

Contributed by @apex-system (PR #2131).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: cancel superseded actionlint and skill-docs runs

actionlint.yml and skill-docs.yml trigger on both push and pull_request
with no concurrency group, so every push to an active branch left the
previous (now-obsolete) runs queued or running — twice per commit on
same-repo PR branches. Add the same cancel-in-progress concurrency
groups the heavier workflows already use, plus a free static tripwire
test that fails CI if a push+pull_request workflow ever ships again
without cancel-in-progress.

Contributed by @jbetala7 (PR #2053).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(make-pdf): correct CJK rendering — NUL sentinel hardening, SC-first fonts, CJK quote context

Three CJK fixes in the PDF pipeline:

- smartypants strips stray input NULs up front so document text can never
  forge the U+0000 placeholder sentinel and leak a preserved-zone marker
  into the output.
- The CJK font stack led with Japanese families, so Simplified-Chinese
  text rendered han glyphs with JP variants. Lead with PingFang SC /
  Heiti SC / Noto Sans CJK SC / Source Han Sans SC before the JP
  fallbacks.
- Quote-smartening only recognized ASCII openers as "start of quote"
  context; the fullwidth colon and CJK brackets now count, so quotes
  after them curl the right way.

Contributed by @rssprivacy-commits (PR #2012).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: regenerate skill output for the quick-win resolver changes

Regen for the deploy-config URL-scheme fix (utility resolver), telemetry
completion-status resolver, and $HOME-doubling binary-resolver fix; ship
goldens refreshed to match. Generated-output-only commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): cached identity is sticky — heal ONLY the provable subdir-cache bug shape

The walk-up rewrite recomputed the slug on every run and "healed" the cache
toward the fresh value, which broke the #2212 continuity contract: a project
that used gstack before adopting a git remote would be silently renamed to
the remote-derived slug, orphaning everything under ~/.gstack/projects/.
Cached identity now wins, with one precise exception: when the cached value
equals THIS pwd's basename while the walk-up proves pwd is not the project
root, the entry came from the pre-walk-up subdirectory bug (#1125) and is
recomputed. All four slug contracts pass together (repo-mode #2212,
walk-up #1125, sanitize, user-slug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(claude): stop false-blocking macOS keychain subscription auth in host detection

The /claude skill's auth probe only recognized env-var/API-key auth, so
macOS subscription installs (keychain-backed, where `claude -p` works fine)
were told they had no auth. Detection now uses host invocation.

Fixes #1890.

Contributed by @xing-qnex (PR #2411); PR #2548 by @shawnacalia covered the
keychain case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(setup): Ubuntu 26.04 Playwright platform detect + silence the codesign false alarm

Two small setup papercuts: the Playwright platform probe now recognizes
Ubuntu 26.04 instead of falling to the generic-Linux path, and macOS
installs stop warning about a codesign "failure" that was actually the
expected unsigned-adhoc path (the real signature check already gates
binary launch).

Contributed by @nuga0718 (PR #2113) and @lucascaro (PR #1758).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): land-and-deploy squash readback, next-version paths, embed-flags quoting

Three template one-liners: land-and-deploy reads the squash-merge result
from the merge commit instead of the stale branch tip; review/landing-report
/land-and-deploy templates call bin/gstack-next-version via its installed
path instead of a bare repo-relative one; setup-gbrain quotes
GBRAIN_EMBED_FLAGS so zsh word-splitting stops silently dropping
voyage-code-3 flags. Regenerated output included.

Contributed by @stormeoio (PR #2011), @rjmurillo (PR #1820) and
@trevorhstandridge (PR #1817).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* release: v1.64.0.0 — fix wave CHANGELOG, VERSION, deferred-wave TODOs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: refresh ship goldens for the telemetry error-field resolver output

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(redact-prepush): assemble the fake AWS key at runtime — the literal blocked our own push

The hook's fixtures carried a live-format AKIA literal, and the repo's own
pre-push scanner (hardened in this wave) correctly blocked pushing it. The
placeholder-suppressed docs key would defeat the detection tests, so the
fixtures now concatenate the key at runtime: tests still exercise real
detection, and the pushed diff never contains a scannable credential shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(slug): terminate the marker walk-up on dirname's fixed point — hung every bin on Windows

Under git-bash on Windows a mixed-form path walks C:/Users -> C: -> . -> .
forever: dirname's fixed point there is never "/", so the walk-up loop spun
and every bin that evals gstack-slug (learnings-log first among them) hung
until spawn timeout. Caught by windows-free-tests CI on the wave PR. Break
on the fixed point itself with a depth cap for exotic forms; regression
tests drive the extracted function with hostile path shapes under a hard
timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 22:02:07 -07:00
Garry TanandClaude Opus 4.8 421460f03a v1.57.8.0 feat: browse js/eval --out render-to-file (canonical Chromium for offline rendering) (#1929)
* feat(browse): js/eval --out render-to-file with write-capability gate

Add --out <file> / --raw to js and eval so an evaluate result is written
straight to disk (base64 data URLs auto-decoded to bytes, charset-validated
before decode, parent dirs created) instead of serialized back through the
CLI. --out is modeled as a per-invocation WRITE: it requires write scope, is
never dispatchable over the pair-agent tunnel (canDispatchOverTunnel now
consults args), and counts as a mutation for watch-mode and tab-ownership.
Shared parseOutArgs/hasOutArg/resultToString helpers keep the handler and the
gate in sync. Tests cover the parser, render-to-file paths, and tunnel guards.

* docs(browse): offline render mode + canonical-Chromium guidance

Document the blessed offline-render path (headless, no proxy/Xvfb): visual
output via screenshot --selector, bytes a function returns via js --out.
Add the puppeteer->browse cheatsheet row, a "don't bundle your own Chromium"
note (browse skill + CONTRIBUTING), and the --out/--raw command descriptions.
Regenerate browse/SKILL.md, SKILL.md, and gstack/llms.txt from the templates.

* chore: bump version and changelog (v1.59.1.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document js/eval --out render-to-file in BROWSER.md reference (v1.59.1.0)

The js and eval reference rows in BROWSER.md drifted: every other reference
surface (SKILL.md, gstack/llms.txt, browse/SKILL.md) already shows the new
[--out <file>] [--raw] flags from v1.59.1.0, but the complete browser
reference still showed the pre-feature signatures. Add the flags plus the
WRITE-capability / no-tunnel note so the reference matches what shipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: re-version 1.59.1.0 -> 1.57.8.0 (natural PATCH from 1.57.7.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:02:30 -07:00
Garry TanandClaude Opus 4.7 c15b805cd8 feat(browse): Puppeteer parity — load-html, screenshot --selector, viewport --scale, file:// (v1.1.0.0) (#1062)
* feat(browse): TabSession loadedHtml + command aliases + DX polish primitives

Adds the foundation layer for Puppeteer-parity features:

- TabSession.loadedHtml + setTabContent/getLoadedHtml/clearLoadedHtml —
  enables load-html content to survive context recreation (viewport --scale)
  via in-memory replay. ASCII lifecycle diagram in the source explains the
  clear-before-navigation contract.

- COMMAND_ALIASES + canonicalizeCommand() helper — single source of truth
  for name aliases (setcontent / set-content / setContent → load-html),
  consumed by server dispatch and chain prevalidation.

- buildUnknownCommandError() pure function — rich error messages with
  Levenshtein-based "Did you mean" suggestions (distance ≤ 2, input
  length ≥ 4 to skip 2-letter noise) and NEW_IN_VERSION upgrade hints.

- load-html registered in WRITE_COMMANDS + SCOPE_WRITE so scoped write
  tokens can use it.

- screenshot and viewport descriptions updated for upcoming flags.

- New browse/test/dx-polish.test.ts (15 tests): alias canonicalization,
  Levenshtein threshold + alphabetical tiebreak, short-input guard,
  NEW_IN_VERSION upgrade hint, alias + scope integration invariants.

No consumers yet — pure additive foundation. Safe to bisect on its own.

* feat(browse): accept file:// in goto with smart cwd/home-relative parsing

Extends validateNavigationUrl to accept file:// URLs scoped to safe dirs
(cwd + TEMP_DIR) via the existing validateReadPath policy. The workhorse is a
new normalizeFileUrl() helper that handles non-standard relative forms BEFORE
the WHATWG URL parser sees them:

    file:///abs/path.html       → unchanged
    file://./docs/page.html     → file://<cwd>/docs/page.html
    file://~/Documents/page.html → file://<HOME>/Documents/page.html
    file://docs/page.html       → file://<cwd>/docs/page.html
    file://localhost/abs/path   → unchanged
    file://host.example.com/... → rejected (UNC/network)
    file:// and file:///        → rejected (would list a directory)

Host heuristic rejects segments with '.', ':', '\\', '%', IPv6 brackets, or
Windows drive-letter patterns — so file://docs.v1/page.html, file://127.0.0.1/x,
file://[::1]/x, and file://C:/Users/x are explicit errors.

Uses fileURLToPath() + pathToFileURL() from node:url (never string-concat) so
URL escapes like %20 decode correctly and Node rejects encoded-slash traversal
(%2F..%2F) outright.

Signature change: validateNavigationUrl now returns Promise<string> (the
normalized URL) instead of Promise<void>. Existing callers that ignore the
return value still compile — they just don't benefit from smart-parsing until
updated in follow-up commits. Callers will be migrated in the next few commits
(goto, diff, newTab, restoreState).

Rewrites the url-validation test file: updates existing tests for the new
return type, adds 20+ new tests covering every normalizeFileUrl shape variant,
URL-encoding edge cases, and path-traversal rejection.

References: codex consult v3 P1 findings on URL parser semantics and fileURLToPath.

* feat(browse): BrowserManager deviceScaleFactor + setContent replay + file:// plumbing

Three tightly-coupled changes to BrowserManager, all in service of the
Puppeteer-parity workflow:

1. deviceScaleFactor + currentViewport tracking. New private fields (default
   scale=1, viewport=1280x720) + setDeviceScaleFactor(scale, w, h) method.
   deviceScaleFactor is a context-level Playwright option — changing it
   requires recreateContext(). The method validates (finite number, 1-3 cap,
   headed-mode rejected), stores new values, calls recreateContext(), and
   rolls back the fields on failure so a bad call doesn't leave inconsistent
   state. Context options at all three sites (launch, recreate happy path,
   recreate fallback) now honor the stored values instead of hardcoding
   1280x720.

2. BrowserState.loadedHtml + loadedHtmlWaitUntil. saveState captures per-tab
   loadedHtml from the session; restoreState replays it via newSession.
   setTabContent() — NOT bare page.setContent() — so TabSession.loadedHtml
   is rehydrated and survives *subsequent* scale changes. In-memory only,
   never persisted to disk (HTML may contain secrets or customer data).

3. newTab + restoreState now consume validateNavigationUrl's normalized
   return value. file://./x, file://~/x, and bare-segment forms now take
   effect at every navigation site, not just the top-level goto command.

Together these enable: load-html → viewport --scale 2 → viewport --scale 1.5
→ screenshot, with content surviving both context recreations. Codex v2 P0
flagged that bare page.setContent in restoreState would lose content on the
second scale change — this commit implements the rehydration path.

References: codex v2 P0 (TabSession rehydration), codex v3 P1 (4-caller
return value), plan Feature 3 + Feature 4.

* feat(browse): load-html, screenshot --selector, viewport --scale, alias dispatch

Wires the new handlers and dispatch logic that the previous commits made
possible:

write-commands.ts
- New 'load-html' case: validateReadPath for safe-dir scoping, stat-based
  actionable errors (not found, directory, oversize), extension allowlist
  (.html/.htm/.xhtml/.svg), magic-byte sniff with UTF-8 BOM strip accepting
  any <[a-zA-Z!?] markup opener (not just <!doctype — bare fragments like
  <div>...</div> work for setContent), 50MB cap via GSTACK_BROWSE_MAX_HTML_BYTES
  override, frame-context rejection. Calls session.setTabContent() so replay
  metadata is rehydrated.
- viewport command extended: optional [<WxH>], optional [--scale <n>],
  scale-only variant reads current size via page.viewportSize(). Invalid
  scale (NaN, Infinity, empty, out of 1-3) throws with named value. Headed
  mode rejected explicitly.
- clearLoadedHtml() called BEFORE goto/back/forward/reload navigation
  (not after) so a timed-out goto post-commit doesn't leave stale metadata
  that could resurrect on a later context recreation. Codex v2 P1 catch.
- goto uses validateNavigationUrl's normalized return value.

meta-commands.ts
- screenshot --selector <css> flag: explicit element-screenshot form.
  Rejects alongside positional selector (both = error), preserves --clip
  conflict at line 161, composes with --base64 at lines 168-174.
- chain canonicalizes each step with canonicalizeCommand — step shape is
  now { rawName, name, args } so prevalidation, dispatch, WRITE_COMMANDS.has,
  watch blocking, and result labels all use canonical names while audit
  labels show 'rawName→name' when aliased. Codex v3 P2 catch — prior shape
  only canonicalized at prevalidation and diverged everywhere else.
- diff command consumes validateNavigationUrl return value for both URLs.

server.ts
- Command canonicalization inserted immediately after parse, before scope /
  watch / tab-ownership / content-wrapping checks. rawCommand preserved for
  future audit (not wired into audit log in this commit — follow-up).
- Unknown-command handler replaced with buildUnknownCommandError() from
  commands.ts — produces 'Unknown command: X. Did you mean Y?' with optional
  upgrade hint for NEW_IN_VERSION entries.

security-audit-r2.test.ts
- Updated chain-loop marker from 'for (const cmd of commands)' to
  'for (const c of commands)' to match the new chain step shape. Same
  isWatching + BLOCKED invariants still asserted.

* chore: bump version and changelog (v1.1.0.0)

- VERSION: 1.0.0.0 → 1.1.0.0 (MINOR bump — new user-facing commands)
- package.json: matching version bump
- CHANGELOG.md: new 1.1.0.0 entry describing load-html, screenshot --selector,
  viewport --scale, file:// support, setContent replay, and DX polish in user
  voice with a dedicated Security section for file:// safe-dirs policy
- browse/SKILL.md.tmpl: adds pattern #12 "Render local HTML", pattern #13
  "Retina screenshots", and a full Puppeteer → browse cheatsheet with side-by-
  side API mapping and a worked tweet-renderer migration example
- browse/SKILL.md + SKILL.md: regenerated from templates via `bun run gen:skill-docs`
  to reflect the new command descriptions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: pre-landing review fixes (9 findings from specialist + adversarial review)

Adversarial review (Claude subagent + Codex) surfaced 9 bugs across
CRITICAL/HIGH severity. All fixed:

1. tab-session.ts:setTabContent — state mutation moved AFTER the setContent
   await. Prior order left phantom HTML in replay metadata if setContent
   threw (timeout, browser crash), which a later viewport --scale would
   silently replay. Now loadedHtml is only recorded on successful load.

2. browser-manager.ts:setDeviceScaleFactor — rollback now forces a second
   recreateContext after restoring the old fields. The fallback path in
   the original recreateContext builds a blank context using whatever
   this.deviceScaleFactor/currentViewport hold at that moment (which were
   the NEW values we were trying to apply). Rolling back the fields without
   a second recreate left the live context at new-scale while state tracked
   old-scale. Now: restore fields, force re-recreate with old values, only
   if that ALSO fails do we return a combined error.

3. commands.ts:buildUnknownCommandError — Levenshtein tiebreak simplified
   to 'd <= 2 && d < bestDist' (strict less). Candidates are pre-sorted
   alphabetically, so first equal-distance wins by default. The prior
   '(d === bestDist && best !== undefined && cand < best)' clause was dead
   code.

4. tab-session.ts:onMainFrameNavigated — now clears loadedHtml, not just
   refs + frame. Without this, a user who load-html'd then clicked a link
   (or had a form submit / JS redirect / OAuth flow) would retain the stale
   replay metadata. The next viewport --scale would silently revert the
   tab to the ORIGINAL loaded HTML, losing whatever the post-navigation
   content was. Silent data corruption. Browser-emitted navigations trigger
   this path via wirePageEvents.

5. browser-manager.ts:saveState + restoreState — tab ownership now flows
   through BrowserState.owner. Without this, a scoped agent's viewport
   --scale would strand them: tab IDs change during recreate, ownership
   map held stale IDs, owner lookup failed. New IDs had no owner, so
   writes without tabId were denied (DoS). Worse, if the agent sent a
   stale tabId the server's swallowed-tab-switch-error path would let the
   command hit whatever tab was currently active (cross-tab authz bypass).
   Now: clear ownership before restore, re-add per-tab with new IDs.

6. meta-commands.ts:state load — disk-loaded state.pages is now explicit
   allowlist (url, isActive, storage:null) instead of object spread.
   Spreading accepted loadedHtml, loadedHtmlWaitUntil, and owner from a
   user-writable state file, letting a tampered state.json smuggle HTML
   past load-html's safe-dirs / extension / magic-byte / 50MB-cap
   validators, or forge tab ownership. Now stripped at the boundary.

7. url-validation.ts:normalizeFileUrl — preserves query string + fragment
   across normalization. file://./app.html?route=home#login previously
   resolved to a filesystem path that URL-encoded '?' as %3F and '#' as
   %23, or (for absolute forms) pathToFileURL dropped them entirely. SPAs
   and fixture URLs with query params 404'd or loaded the wrong route.
   Now: split on ?/# before path resolution, reattach after.

8. url-validation.ts:validateNavigationUrl — reattaches parsed.search +
   parsed.hash to the normalized file:// URL. Same fix at the main
   validator for absolute paths that go through fileURLToPath round-trip.

9. server.ts:writeAuditEntry — audit entries now include aliasOf when the
   user typed an alias ('setcontent' → cmd: 'load-html', aliasOf:
   'setcontent'). Previously the isAliased variable was computed but
   dropped, losing the raw input from the forensic trail. Completes the
   plan's codex v3 P2 requirement.

Also added bm.getCurrentViewport() and switched 'viewport --scale'-
without-size to read from it (more reliable than page.viewportSize() on
headed/transition contexts).

Tests pass: exit 0, no failures. Build clean.

* test: integration coverage for load-html, screenshot --selector, viewport --scale, replay, aliases

Adds 28 Playwright-integration tests that close the coverage gap flagged
by the ship-workflow coverage audit (50% → expected ~80%+).

**load-html (12 tests):**
- happy path loads HTML file, page text matches
- bare HTML fragments (<div>...</div>) accepted, not just full documents
- missing file arg throws usage
- non-.html extension rejected by allowlist
- /etc/passwd.html rejected by safe-dirs policy
- ENOENT path rejected with actionable "not found" error
- directory target rejected
- binary file (PNG magic bytes) disguised as .html rejected by magic-byte check
- UTF-8 BOM stripped before magic-byte check — BOM-prefixed HTML accepted
- --wait-until networkidle exercises non-default branch
- invalid --wait-until value rejected
- unknown flag rejected

**screenshot --selector (5 tests):**
- --selector flag captures element, validates Screenshot saved (element)
- conflicts with positional selector (both = error)
- conflicts with --clip (mutually exclusive)
- composes with --base64 (returns data:image/png;base64,...)
- missing value throws usage

**viewport --scale (5 tests):**
- WxH --scale 2 produces PNG with 2x element dimensions (parses IHDR bytes 16-23)
- --scale without WxH keeps current size + applies scale
- non-finite value (abc) throws "not a finite number"
- out-of-range (4, 0.5) throws "between 1 and 3"
- missing value throws

**setContent replay across context recreation (3 tests):**
- load-html → viewport --scale 2: content survives (hits setTabContent replay path)
- double cycle 2x → 1.5x: content still survives (proves TabSession rehydration)
- goto after load-html clears replay: subsequent viewport --scale does NOT
  resurrect the stale HTML (validates the onMainFrameNavigated fix)

**Command aliases (2 tests):**
- setcontent routes to load-html via chain canonicalization
- set-content (hyphenated) also routes — both end-to-end through chain dispatch

Fixture paths use /tmp (SAFE_DIRECTORIES entry) instead of $TMPDIR which is
/var/folders/... on macOS and outside the safe-dirs boundary. Chain result
labels use rawName→name format when an alias is resolved (matches the
meta-commands.ts chain refactor).

Full suite: exit 0, 223/223 pass.

* docs: update BROWSER.md + CHANGELOG for v1.1.0.0

BROWSER.md:
- Command reference table updated: goto now lists file:// support,
  load-html added to Navigate row, viewport flagged with --scale
  option, screenshot row shows --selector + --base64 flags
- Screenshot modes table adds the fifth mode (element crop via
  --selector flag) and notes the tag-selector-not-caught-positionally
  gotcha
- New "Retina screenshots — viewport --scale" subsection explains
  deviceScaleFactor mechanics, context recreation side effects, and
  headed-mode rejection
- New "Loading local HTML — goto file:// vs load-html" subsection
  explains the two paths, their tradeoffs (URL state, relative asset
  resolution), the safe-dirs policy, extension allowlist + magic-byte
  sniff, 50MB cap, setContent replay across recreateContext, and the
  alias routing (setcontent → load-html before scope check)

CHANGELOG.md (v1.1.0.0 security section expanded, no existing content
removed):
- State files cannot smuggle HTML or forge tab ownership (allowlist
  on disk-loaded page fields)
- Audit log records aliasOf when a canonical command was reached via
  an alias (setcontent → load-html)
- load-html content clears on real navigations (clicks, form submits,
  JS redirects) — not just explicit goto. Also notes SPA query/fragment
  preservation for goto file://

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 23:25:33 +08:00
7e96fe299b fix: security wave 3 — 12 fixes, 7 contributors (v0.16.4.0) (#988)
* fix(security): validateOutputPath symlink bypass — check file-level symlinks

validateOutputPath() previously only resolved symlinks on the parent directory.
A symlink at /tmp/evil.png → /etc/crontab passed the parent check (parent is
/tmp, which is safe) but the write followed the symlink outside safe dirs.

Add lstatSync() check: if the target file exists and is a symlink, resolve
through it and verify the real target is within SAFE_DIRECTORIES. ENOENT
(file doesn't exist yet) falls through to the existing parent-dir check.

Closes #921

Co-Authored-By: Yunsu <Hybirdss@users.noreply.github.com>

* fix(security): shell injection in bin/ scripts — use env vars instead of interpolation

gstack-settings-hook interpolated $SETTINGS_FILE directly into bun -e
double-quoted blocks. A path containing quotes or backticks breaks the JS
string context, enabling arbitrary code execution.

Replace direct interpolation with environment variables (process.env).
Same fix applied to gstack-team-init which had the same pattern.

Systematic audit confirmed only these two scripts were vulnerable — all
other bin/ scripts already use stdin piping or env vars.

Closes #858

Co-Authored-By: Gus <garagon@users.noreply.github.com>

* fix(security): cookie-import path validation bypass + hardcoded /tmp

Two fixes:
1. cookie-import relative path bypass (#707): path.isAbsolute() gated the
   entire validation, so relative paths like "sensitive-file.json" bypassed
   the safe-directory check entirely. Now always resolves to absolute path
   with realpathSync for symlink resolution, matching validateOutputPath().

2. Hardcoded /tmp in cookie-import-browser (#708): openDbFromCopy used
   /tmp directly instead of os.tmpdir(), breaking Windows support.

Also adds explicit imports for SAFE_DIRECTORIES and isPathWithin in
write-commands.ts (previously resolved implicitly through bundler).

Closes #852

Co-Authored-By: Toby Morning <urbantech@users.noreply.github.com>

* fix(security): redact form fields with sensitive names, not just type=password

Form redaction only applied to type="password" fields. Hidden and text
fields named csrf_token, api_key, session_id, etc. were exposed unredacted
in LLM context, leaking secrets.

Extend redaction to check field name and id against sensitive patterns:
token, secret, key, password, credential, auth, jwt, session, csrf, sid,
api_key. Uses the same pattern style as SENSITIVE_COOKIE_NAME.

Closes #860

Co-Authored-By: Gus <garagon@users.noreply.github.com>

* fix(security): restrict session file permissions to owner-only

Design session files written to /tmp with default umask (0644) were
world-readable on shared systems. Sessions contain design prompts and
feedback history.

Set mode 0o600 (owner read/write only) on both create and update paths.

Closes #859

Co-Authored-By: Gus <garagon@users.noreply.github.com>

* fix(security): enforce frozen lockfile during setup

bun install without --frozen-lockfile resolves ^semver ranges from npm on
every run. If an attacker publishes a compromised compatible version of any
dependency, the next ./setup pulls it silently.

Add --frozen-lockfile with fallback to plain install (for fresh clones
where bun.lock may not exist yet). Matches the pattern already used in
the .agents/ generation block (line 237).

Closes #614

Co-Authored-By: Alberto Martinez <halbert04@users.noreply.github.com>

* fix: remove duplicate recursive chmod on /tmp in Dockerfile.ci

chmod -R 1777 /tmp recursively sets sticky bit on files (no defined
behavior), not just the directory. Deduplicate to single chmod 1777 /tmp.

Closes #747

Co-Authored-By: Maksim Soltan <Gonzih@users.noreply.github.com>

* fix(security): learnings input validation + cross-project trust gate

Three fixes to the learnings system:

1. Input validation in gstack-learnings-log: type must be from allowed list,
   key must be alphanumeric, confidence must be 1-10 integer, source must
   be from allowed list. Prevents injection via malformed fields.

2. Prompt injection defense: insight field checked against 10 instruction-like
   patterns (ignore previous, system:, override, etc.). Rejected with clear
   error message.

3. Cross-project trust gate in gstack-learnings-search: AI-generated learnings
   from other projects are filtered out. Only user-stated learnings cross
   project boundaries. Prevents silent prompt injection across codebases.

Also adds trusted field (true for user-stated source, false for AI-generated)
to enable the trust gate at read time.

Closes #841

Co-Authored-By: Ziad Al Sharif <Ziadstr@users.noreply.github.com>

* feat(security): track cookie-imported domains and scope cookie imports

Foundation for origin-pinned JS execution (#616). Tracks which domains
cookies were imported from so the JS/eval commands can verify execution
stays within imported origins.

Changes:
- BrowserManager: new cookieImportedDomains Set with track/get/has methods
- cookie-import: tracks imported cookie domains after addCookies
- cookie-import-browser: tracks domains on --domain direct import
- cookie-import-browser --all: new explicit opt-in for all-domain import
  (previously implicit behavior, now requires deliberate flag)

Closes #615

Co-Authored-By: Alberto Martinez <halbert04@users.noreply.github.com>

* feat(security): pin JS/eval execution to cookie-imported origins

When cookies have been imported for specific domains, block JS execution
on pages whose origin doesn't match. Prevents the attack chain:
1. Agent imports cookies for github.com
2. Prompt injection navigates to attacker.com
3. Agent runs js document.cookie → exfiltrates github cookies

assertJsOriginAllowed() checks the current page hostname against imported
cookie domains with subdomain matching (.github.com allows api.github.com).
When no cookies are imported, all origins allowed (nothing to protect).
about:blank and data: URIs are allowed (no cookies at risk).

Depends on #615 (cookie domain tracking).

Closes #616

Co-Authored-By: Alberto Martinez <halbert04@users.noreply.github.com>

* feat(security): add persistent command audit log

Append-only JSONL audit trail for all browse server commands. Unlike
in-memory ring buffers, the audit log persists across restarts and is
never truncated. Each entry records: timestamp, command, args (truncated
to 200 chars), page origin, duration, status, error (truncated to 300
chars), hasCookies flag, connection mode.

All writes are best-effort — audit failures never block command execution.
Log stored at ~/.gstack/.browse/browse-audit.jsonl.

Closes #617

Co-Authored-By: Alberto Martinez <halbert04@users.noreply.github.com>

* fix(security): block hex-encoded IPv4-mapped IPv6 metadata bypass

URL constructor normalizes ::ffff:169.254.169.254 to ::ffff:a9fe:a9fe
(hex form), which was not in the blocklist. Similarly, ::169.254.169.254
normalizes to ::a9fe:a9fe.

Add both hex-encoded forms to BLOCKED_METADATA_HOSTS so they're caught
by the direct hostname check in validateNavigationUrl.

Closes #739

Co-Authored-By: Osman Mehmood <mehmoodosman@users.noreply.github.com>

* chore: bump version and changelog (v0.16.4.0)

Security wave 3: 12 fixes, 7 contributors.
Cookie origin pinning, command audit log, domain tracking.
Symlink bypass, path validation, shell injection, form redaction,
learnings injection, IPv6 SSRF, session permissions, frozen lockfile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yunsu <Hybirdss@users.noreply.github.com>
Co-authored-by: Gus <garagon@users.noreply.github.com>
Co-authored-by: Toby Morning <urbantech@users.noreply.github.com>
Co-authored-by: Alberto Martinez <halbert04@users.noreply.github.com>
Co-authored-by: Maksim Soltan <Gonzih@users.noreply.github.com>
Co-authored-by: Ziad Al Sharif <Ziadstr@users.noreply.github.com>
Co-authored-by: Osman Mehmood <mehmoodosman@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:49:37 -10:00
1868636f49 refactor: extract TabSession for per-tab state isolation (v0.15.16.0) (#873)
* plan: batch command endpoint + multi-tab parallel execution for GStack Browser

* refactor: extract TabSession from BrowserManager for per-tab state

Move per-tab state (refMap, lastSnapshot, frame) into a new TabSession
class. BrowserManager delegates to the active TabSession via
getActiveSession(). Zero behavior change — all existing tests pass.

This is the foundation for the /batch endpoint: both /command and /batch
will use the same handler functions with TabSession, eliminating shared
state races during parallel tab execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: update handler signatures to use TabSession

Change handleReadCommand and handleSnapshot to take TabSession instead of
BrowserManager. Change handleWriteCommand to take both TabSession (per-tab
ops) and BrowserManager (global ops like viewport, headers, dialog).
handleMetaCommand keeps BrowserManager for tab management.

Tests use thin wrapper functions that bridge the old 3-arg call pattern to
the new signatures via bm.getActiveSession().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add POST /batch endpoint for parallel multi-tab execution

Execute multiple commands across tabs in a single HTTP request.
Commands targeting different tabs run concurrently via Promise.allSettled.
Commands targeting the same tab run sequentially within that group.

Features:
- Batch-safe command subset (text, goto, click, snapshot, screenshot, etc.)
- newtab/closetab as special commands within batch
- SSE streaming mode (stream: true) for partial results
- Per-command error isolation (one tab failing doesn't abort the batch)
- Max 50 commands per batch, soft batch-level timeout

A 143-page crawl drops from ~45 min (serial HTTP) to ~5 min (20 tabs
in parallel, batched commands).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add batch endpoint integration tests

10 tests covering:
- Multi-tab parallel execution (goto + text on different tabs)
- Same-tab sequential ordering
- Per-command error isolation (one tab fails, others succeed)
- Page-scoped refs (snapshot refs are per-session, not global)
- Per-tab lastSnapshot (snapshot -D with independent baselines)
- getSession/getActiveSession API
- Batch-safe command subset validation
- closeTab via page.close preserves at-least-one-page invariant
- Parallel goto on 3 tabs simultaneously

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: harden codex-review E2E — extract SKILL.md section, bump maxTurns to 25

The test was copying the full 55KB/1075-line codex SKILL.md into the fixture,
requiring 8 Read calls just to consume it and exhausting the 15-turn budget
before reaching the actual codex review command. Now extracts only the
review-relevant section (~6KB/148 lines), reducing Read calls from 8 to 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: move batch endpoint plan into BROWSER.md as feature documentation

The batch endpoint is implemented — document it as an actual feature in
BROWSER.md (architecture, API shape, design decisions, usage pattern)
and remove the standalone plan file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.15.16.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: gstack <ship@gstack.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:23:36 -07:00
03973c2fab fix: community security wave — 8 PRs, 4 contributors (v0.15.13.0) (#847)
* fix(bin): pass search params via env vars (RCE fix) (#819)

Replace shell string interpolation with process.env in gstack-learnings-search
to prevent arbitrary code execution via crafted learnings entries. Also fixes
the CROSS_PROJECT interpolation that the original PR missed.

Adds 3 regression tests verifying no shell interpolation remains in the bun -e block.

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): add path validation to upload command (#821)

Add isPathWithin() and path traversal checks to the upload command,
blocking file exfiltration via crafted upload paths. Uses existing
SAFE_DIRECTORIES constant instead of a local copy. Adds 3 regression tests.

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): symlink resolution in meta-commands validateOutputPath (#820)

Add realpathSync to validateOutputPath in meta-commands.ts to catch
symlink-based directory escapes in screenshot, pdf, and responsive
commands. Resolves SAFE_DIRECTORIES through realpathSync to handle
macOS /tmp -> /private/tmp symlinks. Existing path validation tests
pass with the hardened implementation.

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add uninstall instructions to README (#812)

Community PR #812 by @0531Kim. Adds two uninstall paths: the gstack-uninstall
script (handles everything) and manual removal steps for when the repo isn't
cloned. Includes CLAUDE.md cleanup note and Playwright cache guidance.

Co-Authored-By: 0531Kim <0531Kim@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): Windows launcher extraEnv + headed-mode token (#822)

Community PR #822 by @pieterklue. Three fixes:
1. Windows launcher now merges extraEnv into spawned server env (was
   only passing BROWSE_STATE_FILE, dropping all other env vars)
2. Welcome page fallback serves inline HTML instead of about:blank
   redirect (avoids ERR_UNSAFE_REDIRECT on Windows)
3. /health returns auth token in headed mode even without Origin header
   (fixes Playwright Chromium extensions that don't send it)

Also adds HOME/USERPROFILE fallback for cross-platform compatibility.

Co-Authored-By: pieterklue <pieterklue@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): terminate orphan server when parent process exits (#808)

Community PR #808 by @mmporong. Passes BROWSE_PARENT_PID to the spawned
server process. The server polls every 15s with signal 0 and calls
shutdown() if the parent is gone. Prevents orphaned chrome-headless-shell
processes when Claude Code sessions exit abnormally.

Co-Authored-By: mmporong <mmporong@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(security): IPv6 ULA blocking, cookie redaction, per-tab cancel, targeted token (#664)

Community PR #664 by @mr-k-man (security audit round 1, new parts only).

- IPv6 ULA prefix blocking (fc00::/7) in url-validation.ts with false-positive
  guard for hostnames like fd.example.com
- Cookie value redaction for tokens, API keys, JWTs in browse cookies command
- Per-tab cancel files in killAgent() replacing broken global kill-signal
- design/serve.ts: realpathSync upgrade prevents symlink bypass in /api/reload
- extension: targeted getToken handler replaces token-in-health-broadcast
- Supabase migration 003: column-level GRANT restricts anon UPDATE scope
- Telemetry sync: upsert error logging
- 10 new tests for IPv6, cookie redaction, DNS rebinding, path traversal

Co-Authored-By: mr-k-man <mr-k-man@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(security): CSS injection guard, timeout clamping, session validation, tests (#806)

Community PR #806 by @mr-k-man (security audit round 2, new parts only).

- CSS value validation (DANGEROUS_CSS) in cdp-inspector, write-commands, extension inspector
- Queue file permissions (0o700/0o600) in cli, server, sidebar-agent
- escapeRegExp for frame --url ReDoS fix
- Responsive screenshot path validation with validateOutputPath
- State load cookie filtering (reject localhost/.internal/metadata cookies)
- Session ID format validation in loadSession
- /health endpoint: remove currentUrl and currentMessage fields
- QueueEntry interface + isValidQueueEntry validator for sidebar-agent
- SIGTERM->SIGKILL escalation in timeout handler
- Viewport dimension clamping (1-16384), wait timeout clamping (1s-300s)
- Cookie domain validation in cookie-import and cookie-import-browser
- DocumentFragment-based tab switching (XSS fix in sidepanel)
- pollInProgress reentrancy guard for pollChat
- toggleClass/injectCSS input validation in extension inspector
- Snapshot annotated path validation with realpathSync
- 714-line security-audit-r2.test.ts + 33-line learnings-injection.test.ts

Co-Authored-By: mr-k-man <mr-k-man@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.15.13.0)

Community security wave: 8 PRs from 4 contributors (@garagon, @mr-k-man,
@mmporong, @0531Kim, @pieterklue). IPv6 ULA blocking, cookie redaction,
per-tab cancel signaling, CSS injection guards, timeout clamping, session
validation, DocumentFragment XSS fix, parent process watchdog, uninstall
docs, Windows fixes, and 750+ lines of security regression tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: 0531Kim <0531Kim@users.noreply.github.com>
Co-authored-by: pieterklue <pieterklue@users.noreply.github.com>
Co-authored-by: mmporong <mmporong@users.noreply.github.com>
Co-authored-by: mr-k-man <mr-k-man@users.noreply.github.com>
2026-04-06 00:47:04 -07:00
Garry TanandClaude Opus 4.6 3cda8deec9 fix: security audit round 2 (v0.13.4.0) (#640)
* fix: chrome-cdp localhost-only binding

Restrict Chrome CDP to localhost by adding --remote-debugging-address=127.0.0.1
and --remote-allow-origins to prevent network-accessible debugging sessions.

Clears 1 Socket anomaly (Chrome CDP session exposure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: extension sender validation + message type allowlist

Add sender.id check and ALLOWED_TYPES allowlist to the Chrome extension's
message handler. Defense-in-depth against message spoofing from external
extensions or future externally_connectable changes.

Clears 2 Socket anomalies (extension permissions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: checksum-verified bun install

Replace unverified curl|bash bun installation with checksum-verified
download-then-execute pattern. The install script is downloaded, sha256
verified against a known hash, then executed. Preserves the Bun-native
install path without adding a Node/npm dependency.

Clears Snyk W012 + 3 Socket anomalies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: content trust boundary markers in browse output

Wrap page-content commands (text, html, links, forms, accessibility,
console, dialog, snapshot) with --- BEGIN/END UNTRUSTED EXTERNAL CONTENT ---
markers. Covers direct commands (server.ts), chain sub-commands, and
snapshot output (meta-commands.ts).

Adds PAGE_CONTENT_COMMANDS set and wrapUntrustedContent() helper in
commands.ts (single source of truth, DRY). Expands the SKILL.md trust
warning with explicit processing rules for agents.

Clears Snyk W011 (third-party content exposure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: harden trust boundary markers against escape attacks

- Sanitize URLs in markers (remove newlines, cap at 200 chars) to prevent
  marker injection via history.pushState
- Escape marker strings in content (zero-width space) so malicious pages
  can't forge the END marker to break out of the untrusted block
- Wrap resume command snapshot with trust boundary markers
- Wrap diff command output with trust boundary markers
- Wrap watch stop last snapshot with trust boundary markers

Found by cross-model adversarial review (Claude + Codex).

* chore: bump version and changelog (v0.13.4.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: gitignore .factory/ and remove from tracking

Factory Droid support was removed in this branch. The .factory/ directory
was re-added by merging main (which had v0.13.5.0 Factory support).
Gitignore it so it stays out.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:46:33 -06:00
Garry TanandClaude Opus 4.6 7450b5160b fix: security audit remediation — 12 fixes, 20 tests (v0.13.1.0) (#595)
* fix: remove auth token from /health, secure extension bootstrap (CRITICAL-02 + HIGH-03)

- Remove token from /health response (was leaked to any localhost process)
- Write .auth.json to extension dir for Manifest V3 bootstrap
- sidebar-agent reads token from state file via BROWSE_STATE_FILE env var
- Remove getToken handler from extension (token via health broadcast)
- Extension loads token before first health poll to prevent race condition

* fix: require auth on cookie-picker data routes (CRITICAL-01)

- Add Bearer token auth gate on all /cookie-picker/* data/action routes
- GET /cookie-picker HTML page stays unauthenticated (UI shell)
- Token embedded in served HTML for picker's fetch calls
- CORS preflight now allows Authorization header

* fix: add state file TTL and plaintext cookie warning (HIGH-02)

- Add savedAt timestamp to state save output
- Warn on load if state file older than 7 days
- Auto-delete stale state files (>7 days) on server startup
- Warning about plaintext cookie storage in save message

* fix: innerHTML XSS in extension content script and sidepanel (MEDIUM-01)

- content.js: replace innerHTML with createElement/textContent for ref panel
- sidepanel.js: escape entry.command with escapeHtml() in activity feed
- Both found by security audit + Codex adversarial red team

* fix: symlink bypass in validateReadPath (MEDIUM-02)

- Always resolve to absolute path first (fixes relative path bypass)
- Use realpathSync to follow symlinks before boundary check
- Throw on non-ENOENT realpathSync failures (explicit over silent)
- Resolve SAFE_DIRECTORIES through realpathSync (macOS /tmp → /private/tmp)
- Resolve directory part for non-existent files (ENOENT with symlinked parent)

* fix: freeze hook symlink bypass and prefix collision (MEDIUM-03)

- Add POSIX-portable path resolution (cd + pwd -P, works on macOS)
- Fix prefix collision: /project-evil no longer matches /project freeze dir
- Use trailing slash in boundary check to require directory boundary

* fix: shell script injection in gstack-config and telemetry (MEDIUM-04)

- gstack-config: validate keys (alphanumeric+underscore only)
- gstack-config: use grep -F (fixed string) instead of -E (regex)
- gstack-config: escape sed special chars in values, drop newlines
- gstack-telemetry-log: sanitize REPO_SLUG and BRANCH via json_safe()

* test: 20 security tests for audit remediation

- server-auth: verify token removed from /health, auth on /refs, /activity/*
- cookie-picker: auth required on data routes, HTML page unauthenticated
- path-validation: symlink bypass blocked, realpathSync failure throws
- gstack-config: regex key rejected, sed special chars preserved
- state-ttl: savedAt timestamp, 7-day TTL warning
- telemetry: branch/repo with quotes don't corrupt JSON
- adversarial: sidepanel escapes entry.command, freeze prefix collision

* chore: bump version and changelog (v0.13.1.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: tone down changelog — defense in depth, not catastrophic bugs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 08:35:24 -06:00
b343ba2797 fix: community PRs + security hardening + E2E stability (v0.12.7.0) (#552)
* fix(security): skip hidden directories in skill template discovery

discoverTemplates() scans subdirectories for SKILL.md.tmpl files but
only skips node_modules, .git, and dist. Hidden directories like
.claude/, .agents/, and .codex/ (which contain symlinked skill
installs) were being scanned, allowing a malicious .tmpl in a
symlinked skill to inject into the generation pipeline.

Fix: add !d.name.startsWith('.') to the subdirs() filter. This skips
all dot-prefixed directories, matching the standard convention that
hidden dirs are not source code.

* fix(security): sanitize telemetry JSONL inputs against injection

SKILL, OUTCOME, SESSION_ID, SOURCE, and EVENT_TYPE values go directly
into printf %s for JSONL output. If any contain double quotes,
backslashes, or newlines, the JSON breaks — or worse, injects
arbitrary fields.

Fix: strip quotes, backslashes, and control characters from all
string fields before JSONL construction via json_safe() helper.

* fix(security): validate JSON input in gstack-review-log

gstack-review-log appends its argument directly to a JSONL file with
no validation. Malformed or crafted input could corrupt the review log
or inject arbitrary content.

Fix: validate input is parseable JSON via python3 before appending.
Reject with exit 1 and stderr message if invalid.

* fix: treat relative dot-paths as file paths in screenshot command

Closes #495

* fix: use host-specific co-author trailer in /ship and /document-release

Codex-generated skills hardcoded a Claude co-author trailer in commit
messages. Users running gstack under Codex pushed commits attributed
to the wrong AI assistant.

Add {{CO_AUTHOR_TRAILER}} resolver that emits the correct trailer
based on ctx.host:
  - claude: Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  - codex:  Co-Authored-By: OpenAI Codex <noreply@openai.com>

Replace hardcoded trailers in ship/SKILL.md.tmpl and
document-release/SKILL.md.tmpl with the resolver placeholder.

Fixes #282. Fixes #383.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: auto-upgrade marker no longer masks newer remote versions

When a just-upgraded-from marker persists across sessions, the update
check would write UP_TO_DATE to cache and exit immediately — never
fetching the remote VERSION. Users silently miss updates that landed
after their last upgrade.

Remove the early exit and premature cache write so the script falls
through to the remote check after consuming the marker. This ensures
JUST_UPGRADED is still emitted for the preamble, while also detecting
any newer versions available upstream.

Fixes #515

* fix: decouple doc generation from binary compilation in build script

The build script chains gen:skill-docs and bun build --compile with &&,
so a doc generation failure (e.g. missing Codex host config, template
error) prevents the browse binary from being compiled. Users end up
with a broken install where setup reports the binary is missing.

Replace && with ; for the two gen:skill-docs steps so they run
independently of the compilation chain. Doc generation errors are still
visible in stderr, but no longer block binary compilation.

Fixes #482

* fix: extend security sanitization + add 10 tests for merged community PRs

- Extend json_safe() to ERROR_CLASS and FAILED_STEP fields
- Improve ERROR_MESSAGE escaping to handle backslashes and newlines
- Replace python3 with bun for JSON validation in gstack-review-log
- Add 7 telemetry injection prevention tests
- Add 2 review-log JSON validation tests
- Add 1 discover-skills hidden directory filtering test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: stabilize flaky E2E tests (browse-basic, ship-base-branch, dashboard-via)

browse-basic: bump maxTurns 5→7 (agent reads PNG per SKILL.md instruction)
ship-base-branch: extract Step 0 only instead of full 1900-line ship/SKILL.md
dashboard-via: extract dashboard section only + increase timeout 90s→180s

Root cause: copying full SKILL.md files into test fixtures caused context bloat,
leading to timeouts and flaky turn limits. Extracting only the relevant section
cut dashboard-via from timing out at 240s to finishing in 38s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add E2E fixture extraction rule to CLAUDE.md

Never copy full SKILL.md files into E2E test fixtures. Extract only
the section the test needs. Also: run targeted evals in foreground,
never pkill and restart mid-run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: stabilize journey-think-bigger routing test

Use exact trigger phrases from plan-ceo-review skill description
("think bigger", "expand scope", "ambitious enough") instead of
the ambiguous "thinking too small". Reduce maxTurns 5→3 to cut
cost per attempt ($0.12 vs $0.25). Test remains periodic tier
since LLM routing is inherently non-deterministic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* remove: delete journey-think-bigger routing test

Never passed reliably. Tests ambiguous routing ("think bigger" →
plan-ceo-review) but Claude legitimately answers directly instead
of invoking a skill. The other 10 journey tests cover routing
with clear, actionable signals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.12.7.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Arun Kumar Thiagarajan <arunkt.bm14@gmail.com>
Co-authored-by: bluzername <bluzer@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Greg Jackson <gregario@users.noreply.github.com>
2026-03-26 23:21:27 -06:00
Garry TanandClaude Opus 4.6 7665adf4fe feat: headed mode + sidebar agent + Chrome extension (v0.12.0) (#517)
* feat: CDP connect — control real Chrome/Comet via Playwright

Add `connectCDP()` to BrowserManager: connects to a running browser via
Chrome DevTools Protocol. All existing browse commands work unchanged
through Playwright's abstraction layer.

- chrome-launcher.ts: browser discovery, CDP probe, auto-relaunch with rollback
- browser-manager.ts: connectCDP(), mode guards (close/closeTab/recreateContext/handoff),
  auto-reconnect on browser restart, getRefMap() for extension API
- server.ts: CDP branch in start(), /health gains mode field, /refs endpoint,
  idle timer only resets on /command (not passive endpoints)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: browse connect/disconnect/focus CLI commands

- connect: pre-server command that discovers browser, starts server in CDP mode
- disconnect: drops CDP connection, restarts in headless mode
- focus: brings browser window to foreground via osascript (macOS)
- status: now shows Mode: cdp | launched | headed
- startServer() accepts extra env vars for CDP URL/port passthrough

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: CDP-aware skill templates — skip cookie import in real browser mode

Skills now check `$B status` for CDP mode and skip:
- /qa: cookie import prompt, user-agent override, headless workarounds
- /design-review: cookie import for authenticated pages
- /setup-browser-cookies: returns "not needed" in CDP mode

Regenerated SKILL.md files from updated templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: activity streaming — SSE endpoint for Chrome extension Side Panel

Real-time browse command feed via Server-Sent Events:
- activity.ts: ActivityEntry type, CircularBuffer (capacity 1000), privacy
  filtering (redacts passwords, auth tokens, sensitive URL params),
  cursor-based gap detection, async subscriber notification
- server.ts: /activity/stream SSE, /activity/history REST, handleCommand
  instrumented with command_start/command_end events
- 18 unit tests for filterArgs privacy, emitActivity, subscribe lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: Chrome extension Side Panel + Conductor API proposal

Chrome extension (Manifest V3, sideload):
- Side Panel with live activity feed, @ref overlays, dark terminal aesthetic
- Background worker: health polling, SSE relay, ref fetching
- Popup: port config, connection status, side panel launcher
- Content script: floating ref panel with @ref badges

Conductor API proposal (docs/designs/CONDUCTOR_SESSION_API.md):
- SSE endpoint for full Claude Code session mirroring in Side Panel
- Discovery via HTTP endpoint (not filesystem — extensions can't read files)

TODOS.md: add $B watch, multi-agent tabs, cross-platform CDP, Web Store publishing.
Mark CDP mode as shipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: detect Conductor runtime, skip osascript quit for sandboxed apps

macOS App Management blocks Electron apps (Conductor) from quitting
other apps via osascript. Now detects the runtime environment:
- terminal/claude-code/codex: can manage apps freely
- conductor: prints manual restart instructions + polls for 60s

detectRuntime() checks env vars and parent process. When Chrome needs
restart but we can't quit it, prints step-by-step instructions and
waits for the user to restart Chrome with --remote-debugging-port.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: detect Conductor via actual env vars (CONDUCTOR_WORKSPACE_NAME)

Previous detection checked CONDUCTOR_WORKSPACE_ID which doesn't exist.
Conductor sets CONDUCTOR_WORKSPACE_NAME, CONDUCTOR_BIN_DIR, CONDUCTOR_PORT,
and __CFBundleIdentifier=com.conductor.app. Check these FIRST because
Conductor sessions also have ANTHROPIC_API_KEY (which was matching claude-code).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: connection status pill — floating indicator when gstack controls Chrome

Small pill in bottom-right corner of every page: "● gstack · 3 refs"
Shows when connected via CDP, fades to 30% opacity after 3s, full on hover.
Disappears entirely when disconnected.

Background worker now notifies content scripts on connect/disconnect state
changes so the pill appears/disappears without polling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Chrome requires --user-data-dir for remote debugging

Chrome refuses --remote-debugging-port without an explicit --user-data-dir.
Add userDataDir to BrowserBinary registry (macOS Application Support paths)
and pass it in both auto-launch and manual restart instructions.

Fix double-quoting in CLI manual restart instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Chrome must be fully quit before launching with --remote-debugging-port

Chrome refuses to enable CDP on its default profile when another instance
is running (even with explicit --user-data-dir). The only reliable path:
fully quit Chrome first, then relaunch with the flag.

Updated instructions to emphasize this clearly with verification step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: bin/chrome-cdp — quit Chrome and relaunch with CDP in one command

Quits Chrome gracefully, waits for full exit, relaunches with
--remote-debugging-port, polls until CDP is ready. Usage: chrome-cdp [port]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use Playwright channel:chrome instead of broken connectOverCDP

Playwright's connectOverCDP hangs with Chrome 146 due to CDP protocol
version mismatch. Switch to channel:'chrome' which uses Playwright's
native pipe protocol to launch the system Chrome binary directly.

This is simpler and more reliable:
- No CDP port discovery needed
- No --remote-debugging-port or --user-data-dir hassles
- $B connect just works — launches real Chrome headed window
- All Playwright APIs (snapshot, click, fill) work unchanged

bin/chrome-cdp updated with symlinked profile approach (kept for
manual CDP use cases, but $B connect no longer needs it).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: green border + gstack label on controlled Chrome window

Injects a 2px green border and small "gstack" label on every page
loaded in the controlled Chrome window via context.addInitScript().
Users can instantly tell which Chrome window Claude controls.

Also fixes close() for channel:chrome mode (uses browser.close()
not browser.disconnect() which doesn't exist).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: cleanup chrome-launcher runtime detection, remove puppeteer-core dep

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(design): redesign controlled Chrome indicator

Replace crude green border + label with polished indicator:
- 2px shimmer gradient at top edge (green→cyan→green, 3s loop)
- Floating pill bottom-right with frosted glass bg, fades to 25%
  opacity after 4s so it doesn't compete with page content
- prefers-reduced-motion disables shimmer animation
- Much more subtle — looks like a developer tool, not broken CSS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: document real browser mode + Chrome extension in BROWSER.md and README.md

BROWSER.md: new sections for connect/disconnect/focus commands,
Chrome extension Side Panel install, CDP-aware skills, activity streaming.
Updated command reference table, key components, env vars, source map.

README.md: updated /browse description, added "Real browser mode" to
What's New section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: step-by-step Chrome extension install guide in BROWSER.md

Replace terse bullet points with numbered walkthrough covering:
developer mode toggle, load unpacked, macOS file picker tip (Cmd+Shift+G),
pin extension, configure port, open side panel. Added troubleshooting section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Cmd+Shift+. tip for hidden folders in macOS file picker

macOS hides folders starting with . by default. Added both shortcuts:
Cmd+Shift+G (paste path directly) and Cmd+Shift+. (show hidden files).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: integrate hidden folder tips into the install flow naturally

Move Cmd+Shift+G and Cmd+Shift+. tips inline with the file picker
step instead of as a separate tip block after it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: auto-load Chrome extension when $B connect launches Chrome

Extension auto-loads via --load-extension flag — no manual chrome://extensions
install needed. findExtensionPath() checks repo root, global install, and dev
paths. Also adds bin/gstack-extension helper for manual install in regular
Chrome, and rewrites BROWSER.md install docs with auto-load as primary path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /connect-chrome skill — one command to launch Chrome with Side Panel

New skill that runs $B connect, verifies the connection, guides the user
to open the Side Panel, and demos the live activity feed. Extension auto-loads
via --load-extension so no manual chrome://extensions install needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use launchPersistentContext for Chrome extension loading

Playwright's chromium.launch() silently ignores --load-extension.
Switch to launchPersistentContext with ignoreDefaultArgs to remove
--disable-extensions flag. Use bundled Chromium (real Chrome blocks
unpacked extensions). Fixed port 34567 for CDP mode so the extension
auto-connects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: sync extension to DESIGN.md — amber accent, zinc neutrals, grain texture

Import design system from gstack-website. Update all extension colors:
green (#4ade80) → amber (#F59E0B/#FBBF24), zinc gray neutrals, grain
texture overlay. Regenerate icons as amber "G" monogram on dark background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: sidebar chat with Claude Code — icon opens side panel directly

Replace popup flyout with direct side panel open on icon click. Primary
UI is now a chat interface that sends messages to Claude Code via file
queue. Activity/Refs tabs moved behind a debug toggle in the footer.
Command bar with history, auto-poll for responses, amber design system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: sidebar agent — Claude-powered chat backend via file queue

Add /sidebar-command, /sidebar-response, and /sidebar-chat endpoints
to the browse server. sidebar-agent.ts watches the command queue file,
spawns claude -p with browse context for each message, and streams
responses back to the sidebar chat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove duplicate gstack pill overlay, hide crash restore bubble

The addInitScript indicator and the extension's content script were both
injecting bottom-right pills, causing duplicates. Remove the pill from
addInitScript (extension handles it). Replace --restore-last-session with
--hide-crash-restore-bubble to suppress the "Chromium didn't shut down
correctly" dialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: state file authority — CDP server cannot be silently replaced

Hardens the connect/disconnect lifecycle:
- ensureServer() refuses to auto-start headless when CDP server is alive
- $B connect does full cleanup: SIGTERM → 2s → SIGKILL, profile locks, state
- shutdown() cleans Chromium SingletonLock/Socket/Cookie files
- uncaughtException/unhandledRejection handlers do emergency cleanup

This prevents the bug where a headless server overwrites the CDP server's
state file, causing $B commands to hit the wrong browser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: sidebar agent streaming events + session state management

Enhance sidebar-agent.ts with:
- Live streaming of claude -p events (tool_use, text, result) to sidebar
- Session state file for BROWSE_STATE_FILE propagation to claude subprocess
- Improved logging (stderr, exit codes, event types)
- stdin.end() to prevent claude waiting for input
- summarizeToolInput() with path shortening for compact sidebar display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: sidebar chat UI — streaming events, agent status, reconnect retry

Sidebar panel improvements:
- Chat tab renders streaming agent events (tool_use, text, result)
- Thinking dots animation while agent processes
- Agent error display with styled error blocks
- tryConnect() with 2s retry loop for initial connection
- Debug tabs (Activity/Refs) hidden behind gear toggle
- Clear chat button
- Compact tool call display with path shortening

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: server-integrated sidebar agent with sessions and message queue

Move the sidebar agent from a separate bun process into server.ts:
- Agent spawns claude -p directly when messages arrive via /sidebar-command
- In-memory chat buffer backed by per-session chat.jsonl on disk
- Session manager: create, load, persist, list sessions
- Message queue (cap 5) with agent status tracking (idle/processing/hung)
- Stop/kill endpoints with queue dismiss support
- /health now returns agent status + session info
- All sidebar endpoints require Bearer auth
- Agent killed on server shutdown
- 120s timeout detects hung claude processes

Eliminates: file-queue polling, separate sidebar-agent.ts process,
stale auth tokens, state file conflicts between processes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: extension auth + token flow for server-integrated agent

Update Chrome extension to use Bearer auth on all sidebar endpoints:
- background.js captures auth token from /health, exposes via getToken msg
- background.js sets openPanelOnActionClick for direct side panel access
- sidepanel.js gets token from background, sends in all fetch headers
- Health broadcasts include token so sidebar auto-authenticates
- Removes popup from manifest — icon click opens side panel directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: self-healing sidebar — reconnect banner, state machine, copy button

Sidebar UI now handles disconnection gracefully:
- Connection state machine: connected → reconnecting → dead
- Amber pulsing banner during reconnect (2s retry, 30 attempts)
- Red "Server offline" banner with Reconnect + Copy /connect-chrome buttons
- Green "Reconnected" toast that fades after 3s on successful reconnect
- Copy button lets user paste /connect-chrome into any Claude Code session

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: crash handling — save session, kill agent, distinct exit codes

Hardened shutdown/crash behavior:
- Browser disconnect exits with code 2 (distinct from crash code 1)
- emergencyCleanup kills agent subprocess and saves session state
- Clean shutdown saves session before exit (chat history persists)
- Clear user message on browser disconnect: "Run $B connect to reconnect"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: worktree-per-session isolation for sidebar agent

Each sidebar session gets an isolated git worktree so the agent's file
operations don't conflict with the user's working directory:
- createWorktree() creates detached HEAD worktree in ~/.gstack/worktrees/
- Falls back to main cwd for non-git repos or on creation failure
- Handles collision cleanup from prior crashes
- removeWorktree() cleans up on session switch and shutdown
- worktreePath persisted in session.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(qa): ISSUE-001 — disconnect blocked by CDP guard in ensureServer

$B disconnect was routed through ensureServer() which refused to start a
headless server when a CDP state file existed. Disconnect is now handled
before ensureServer() (like connect), with force-kill + cleanup fallback
when the CDP server is unresponsive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve claude binary path for daemon-spawned agent

The browse server runs as a daemon and may not inherit the user's shell
PATH. Add findClaudeBin() that checks ~/.local/bin/claude (standard
install location), which claude, and common system paths. Shows a clear
error in the sidebar chat if claude CLI is not found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve claude symlinks + check Conductor bundled binary

posix_spawn fails on symlinks in compiled bun binaries. Now:
- Checks Conductor app's bundled binary first (not a symlink)
- Scans ~/.local/share/claude/versions/ for direct versioned binaries
- Uses fs.realpathSync() to resolve symlinks before spawning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: compiled bun binary cannot posix_spawn — use external agent process

Compiled bun binaries fail posix_spawn on ALL executables (even /bin/bash).
The server now writes to an agent queue file, and a separate non-compiled
bun process (sidebar-agent.ts) reads the queue, spawns claude, and POSTs
events back via /sidebar-agent/event.

Changes:
- server.ts: spawnClaude writes to queue file instead of spawning directly
- server.ts: new /sidebar-agent/event endpoint for agent → server relay
- server.ts: fix result event field name (event.text vs event.result)
- sidebar-agent.ts: rewritten to poll queue file, relay events via HTTP
- cli.ts: $B connect auto-starts sidebar-agent as non-compiled bun process

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: loading spinner on sidebar open while connecting to server

Shows an amber spinner with "Connecting..." when the sidebar first opens,
replacing the empty state. After the first successful /sidebar-chat poll:
- If chat history exists: renders it immediately
- If no history: shows the welcome message

Prevents the jarring empty-then-populated flash on sidebar open.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: zero-friction side panel — auto-open on install, pill is clickable

Three changes to eliminate manual side panel setup:
- Auto-open side panel on extension install/update (onInstalled listener)
- gstack pill (bottom-right) is now clickable — opens the side panel
- Pill has pointer-events: auto so clicks always register (was: none)

User no longer needs to find the puzzle piece icon, pin the extension,
or know the side panel exists. It opens automatically on first launch
and can be re-opened by clicking the floating gstack pill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: kill CDP naming, delete chrome-launcher.ts dead code

The connectCDP() method and connectionMode: 'cdp' naming was a legacy
artifact — real Chrome was tried but failed (silently blocks
--load-extension), so the implementation already used Playwright's
bundled Chromium via launchPersistentContext(). The naming was
misleading.

Changes:
- Delete chrome-launcher.ts (361 LOC) — only import was in unreachable
  attemptReconnect() method
- Delete dead attemptReconnect() and reconnecting field
- Delete preExistingTabIds (was for protecting real Chrome tabs we
  never connect to)
- Rename connectCDP() → launchHeaded()
- Rename connectionMode: 'cdp' → 'headed' across all files
- Replace BROWSE_CDP_URL/BROWSE_CDP_PORT env vars with BROWSE_HEADED=1
- Regenerate SKILL.md files for updated command descriptions
- Move BrowserManager unit tests to browser-manager-unit.test.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: converge handoff into connect — extension loads on handoff

Handoff now uses launchPersistentContext() with extension auto-loading,
same as the connect/launchHeaded() path. This means when the agent
gets stuck (2FA, CAPTCHA) and hands off to the user, the Chrome
extension + side panel are available automatically.

Before: handoff used chromium.launch() + newContext() — no extension
After: handoff uses chromium.launchPersistentContext() — extension loads

Also sets connectionMode to 'headed' and disables dialog auto-accept
on handoff, matching connect behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: gate sidebar chat behind --chat flag

$B connect (default): headed Chromium + extension with Activity + Refs
tabs only. No separate agent spawned. Clean, no confusion.

$B connect --chat: same + Chat tab with standalone claude -p agent.
Shows experimental banner: "Standalone mode — this is a separate
agent from your workspace."

Implementation:
- cli.ts: parse --chat, set BROWSE_SIDEBAR_CHAT env, conditionally
  spawn sidebar-agent
- server.ts: gate /sidebar-* routes behind chatEnabled, return 403
  when disabled, include chatEnabled in /health response
- sidepanel.js: applyChatEnabled() hides/shows Chat tab + banner
- background.js: forward chatEnabled from health response
- sidepanel.html/css: experimental banner with amber styling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: file drop relay + $B inbox command

Sidebar agent now writes structured messages to .context/sidebar-inbox/
when processing user input. The workspace agent can read these via
$B inbox to see what the user reported from the browser.

File drop format:
  .context/sidebar-inbox/{timestamp}-observation.json
  { type, timestamp, page: {url}, userMessage, sidebarSessionId }

Atomic writes (tmp + rename) prevent partial reads. $B inbox --clear
removes messages after display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: $B watch — passive observation mode

Claude enters read-only mode and captures periodic snapshots (every 5s)
while the user browses. Mutation commands (click, fill, etc.) are
blocked during watch. $B watch stop exits and returns a summary with
the last snapshot.

Requires headed mode ($B connect). This is the inverse of the scout
pattern — the workspace agent watches through the browser instead of
the sidebar relaying to it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add coverage for sidebar-agent, file-drop, and watch mode

33 new tests covering:
- Sidebar agent queue parsing (valid/malformed/empty JSONL)
- writeToInbox file drop (directory creation, atomic writes, JSON format)
- Inbox command (display, sorting, --clear, malformed file handling)
- Watch mode state machine (start/stop cycles, snapshots, duration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: TODOS cleanup + Chrome vs Chromium exploration doc

- Update TODOS.md: mark CDP mode, $B watch, sidebar scout as SHIPPED
- Delete dead "cross-platform CDP browser discovery" TODO
- Rename dependencies from "CDP connect" to "headed mode"
- Add docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md memorializing
  the architecture exploration and decision to use Playwright Chromium

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Conductor Chrome sidebar integration design doc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sidebar-agent validates cwd before spawning claude

The queue entry may reference a worktree that was cleaned up between
sessions. Now falls back to process.cwd() if the path doesn't exist,
preventing silent spawn failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: gen-skill-docs resolver merge + preamble tier gate + plan file discovery

The local RESOLVERS record in gen-skill-docs.ts was shadowing the imported
canonical resolvers, causing stale test coverage and preamble generators
to be used instead of the authoritative versions in resolvers/.

Changes:
- Merge imported RESOLVERS with local overrides (spread + override pattern)
- Fix preamble tier gate: tier 1 skills no longer get AskUserQuestion format
- Make plan file discovery host-agnostic (search multiple plan dirs)
- Add missing E2E tier entries for ship/review plan completion tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: ungate sidebar agent + raise timeout to 5 minutes (v0.12.0)

Sidebar chat is now always available in headed mode — no --chat flag needed.
Agent tasks get 5 minutes instead of 2, enabling multi-page workflows like
navigating directories and filling forms across pages.

Changes:
- cli.ts: remove --chat flag, always set BROWSE_SIDEBAR_CHAT=1, always spawn agent
- server.ts: remove chatEnabled gate (403 response), raise AGENT_TIMEOUT_MS to 300s
- sidebar-agent.ts: raise child process timeout from 120s to 300s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: headed mode + sidebar agent documentation (v0.12.0)

- README: sidebar agent section, personal automation example (school parent
  portal), two auth paths (manual login + cookie import), DevTools MCP mention
- BROWSER.md: sidebar agent section with usage, timeout, session isolation,
  authentication, and random delay documentation
- connect-chrome template: add sidebar chat onboarding step
- CHANGELOG: v0.12.0 entry covering headed mode, sidebar agent, extension
- VERSION: bump to 0.12.0.0
- TODOS: Chrome DevTools MCP integration as P0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate SKILL.md files

Generated from updated templates + resolver merge. Key changes:
- Tier 1 skills no longer include AskUserQuestion format section
- Ship/review skills now include coverage gate with thresholds
- Connect-chrome skill includes sidebar chat onboarding step
- Plan file discovery uses host-agnostic paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate Codex connect-chrome skill

Updated preamble with proactive prompt and sidebar chat onboarding step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: network idle, state persistence, iframe support, chain pipe format (v0.12.1.0) (#516)

* feat: network idle detection + chain pipe format

- Upgrade click/fill/select from domcontentloaded to networkidle wait
  (2s timeout, best-effort). Catches XHR/fetch triggered by interactions.
- Add pipe-delimited format to chain as JSON fallback:
  $B chain 'goto url | click @e5 | snapshot -ic'
- Add post-loop networkidle wait in chain when last command was a write.
- Frame-aware: commands use target (getActiveFrameOrPage) for locator ops,
  page-only ops (goto/back/forward/reload) guard against frame context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: $B state save/load + $B frame — new browse commands

- state save/load: persist cookies + URLs to .gstack/browse-states/{name}.json
  File perms 0o600, name sanitized to [a-zA-Z0-9_-]. V1 skips localStorage
  (breaks on load-before-navigate). Load replaces session via closeAllPages().
- frame: switch command context to iframe via CSS selector, @ref, --name, or
  --url. 'frame main' returns to main frame. Execution target abstraction
  (getActiveFrameOrPage) across read-commands, snapshot, and write-commands.
- Frame context cleared on tab switch, navigation, resume, and handoff.
- Snapshot shows [Context: iframe src="..."] header when in frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add tests for network idle, chain pipe format, state, and frame

- Network idle: click on fetch button waits for XHR, static click is fast
- Chain pipe: pipe-delimited commands, quoted args, JSON still works
- State: save/load round-trip, name sanitization, missing state error
- Frame: switch to iframe + back, snapshot context header, fill in frame,
  goto-in-frame guard, usage error

New fixtures: network-idle.html (fetch + static buttons), iframe.html (srcdoc)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: review fixes — iframe ref scoping, detached frame recovery, state validation

- snapshot.ts: ref locators, cursor-interactive scan, and cursor locator
  now use target (frame-aware) instead of page — fixes @ref clicking in iframes
- browser-manager.ts: getActiveFrameOrPage auto-recovers from detached frames
  via isDetached() check
- meta-commands.ts: state load resets activeFrame, elementHandle disposed after
  contentFrame(), state file schema validation (cookies + pages arrays),
  filter empty pipe segments in chain tokenizer
- write-commands.ts: upload command uses target.locator() for frame support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate SKILL.md files + rebuild binary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.12.1.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:15:24 -06:00
cf3582c637 fix: community security + stability fixes (wave 1) (#325)
* feat: add /cso skill — OWASP Top 10 + STRIDE security audit

* fix: harden gstack-slug against shell injection via eval

Whitelist safe characters (a-zA-Z0-9._-) in SLUG and BRANCH output
to prevent shell metacharacter injection when used with eval.

Only affects self-hosted git servers with lax naming rules — GitHub
and GitLab enforce safe characters already. Defense-in-depth.

* fix(security): sanitize gstack-slug output against shell injection

The gstack-slug script is consumed via eval $(gstack-slug) throughout
skill templates. If a git remote URL contains shell metacharacters
like $(), backticks, or semicolons, they would be executed by eval.

Fix: strip all characters except [a-zA-Z0-9._-] from both SLUG and
BRANCH before output. This preserves normal values while neutralizing
any injection payload in malicious remote URLs.

Before: eval $(gstack-slug) with remote "foo/bar$(rm -rf /)" → executes rm
After:  eval $(gstack-slug) with remote "foo/bar$(rm -rf /)" → SLUG=foo-barrm-rf-

* fix(security): redact sensitive values in storage command output

The browse `storage` command dumps all localStorage and sessionStorage
as JSON. This can expose tokens, API keys, JWTs, and session credentials
in QA reports and agent transcripts.

Fix: redact values where the key matches sensitive patterns (token,
secret, key, password, auth, jwt, csrf) or the value starts with known
credential prefixes (eyJ for JWT, sk- for Stripe, ghp_ for GitHub, etc.).

Redacted values show length to aid debugging: [REDACTED — 128 chars]

* fix(browse): kill old server before restart to prevent orphaned chromium processes

When the health check fails or the server connection drops, `ensureServer()`
and `sendCommand()` would call `startServer()` without first killing the
previous server process. This left orphaned `chrome-headless-shell` renderer
processes running at ~120% CPU each.

After several reconnect cycles (e.g. pages that crash during hydration or
trigger hard navigations via `window.location.href`), dozens of zombie
chromium processes accumulate and exhaust system resources.

Fix: call `killServer()` on the stale PID before spawning a new server in
both the `ensureServer()` unhealthy path and the `sendCommand()` connection-
lost retry path.

Fixes #294

* Fix YAML linter error: nested mapping in compact sequence entries

Having "Run: bun" inside a plain scalar is not allowed per YAML spec which states: Plain scalars must never contain the “: ” and “ #” character combinations.

This simple fix switches to block scalars (|) to eliminate the ambiguity without changing runtime behavior.

* fix(security): add Azure metadata endpoint to SSRF blocklist

Add metadata.azure.internal to BLOCKED_METADATA_HOSTS alongside the
existing AWS/GCP endpoints. Closes the coverage gap identified in #125.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add coverage for storage redaction

Test key-based redaction (auth_token, api_key), value-based redaction
(JWT prefix, GitHub PAT prefix), pass-through for normal keys, and
length preservation in redacted output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add community PR triage process to CONTRIBUTING.md

Document the wave-based PR triage pattern used for batching community
contributions. References PR #205 (v0.8.3) as the original example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: adjust test key names to avoid redaction pattern collision

Rename testKey→testData and normalKey→displayName in storage tests
to avoid triggering #238's SENSITIVE_KEY regex (which matches 'key').
Also generate Codex variant of /cso skill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.9.10.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: zero-noise /cso security audits with FP filtering (v0.11.0.0)

Absorb Anthropic's security-review false positive filtering into /cso:
- 17 hard exclusions (DOS, test files, log spoofing, SSRF path-only,
  regex injection, race conditions unless concrete, etc.)
- 9 precedents (React XSS-safe, env vars trusted, client-side code
  doesn't need auth, shell scripts need concrete untrusted input path)
- 8/10 confidence gate — below threshold = don't report
- Independent sub-agent verification for each finding
- Exploit scenario requirement per finding
- Framework-aware analysis (Rails CSRF, React escaping, Angular sanitization)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: consolidate CHANGELOG — merge /cso launch + community wave into v0.11.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: rewrite README — lead with Karpathy quote, cut LinkedIn phrases, add /cso

Opens with the revolution (Karpathy, Steinberger/OpenClaw), keeps credentials
and LOC numbers, cuts filler phrases, adds hater bait, restores hiring block,
removes bloated "What's new" section, adds /cso to skills table and install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cso): adversarial review fixes — FP filtering, prompt injection, language coverage

- Exclusion #10: test files must verify not imported by non-test code
- Exclusion #13: distinguish user-message AI input from system-prompt injection
- Exclusion #14: ReDoS in user-input regex IS a real CVE class, don't exclude
- Add anti-manipulation rule: ignore audit-influencing instructions in codebase
- Fix confidence gate: remove contradictory 7-8 tier, hard cutoff at 8
- Fix verifier anchoring: send only file+line, not category/description
- Add Go, PHP, Java, C#, Kotlin to grep patterns (was 4 languages, now 8)
- Add GraphQL, gRPC, WebSocket endpoint detection to attack surface mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(docs): correct skill counts, add /autoplan to README tables

Skill count was wrong in 3 places (said 19+7=26, said 25, actual is 28).
Added /autoplan to specialist table. Fixed troubleshooting skills list
to include all skills added since v0.7.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): DNS rebinding protection for SSRF blocklist

validateNavigationUrl is now async — resolves hostname to IP and checks
against blocked metadata IPs. Prevents DNS rebinding where evil.com
initially resolves to a safe IP, then switches to 169.254.169.254.
All callers updated to await. Tests updated for async assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): lockfile prevents concurrent server start races

Adds exclusive lockfile (O_CREAT|O_EXCL) around ensureServer to prevent
TOCTOU race where two CLI invocations could both kill the old server and
start new ones, leaving an orphaned chromium process. Second caller now
waits for the first to finish starting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): improve storage redaction — word-boundary keys + more value prefixes

Key regex: use underscore/dot/hyphen boundaries instead of \b (which treats
_ as word char). Now correctly redacts auth_token, session_token while
skipping keyboardShortcuts, monkeyPatch, primaryKey.

Value regex: add AWS (AKIA), Stripe (sk_live_, pk_live_), Anthropic (sk-ant-),
Google (AIza), Sendgrid (SG.), Supabase (sbp_) prefixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: migrate all remaining eval callers to source, fix stale CHANGELOG claim

5 templates and 2 bin scripts still used eval $(gstack-slug). All now use
source <(gstack-slug). Updated gstack-slug comment to match. Fixed v0.8.3
CHANGELOG entry that falsely claimed eval was fully eliminated — it was
the output sanitization that made it safe, not a calling convention change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(docs): add /autoplan to install instructions, regen skill docs

The install instruction blocks and troubleshooting section were missing
/autoplan. All three skill list locations now include the complete 28-skill
set. Regenerated codex/agents SKILL.md files to match template changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.11.0.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(cso): add disclaimer — not a substitute for professional security audits

LLMs can miss subtle vulns and produce false negatives. For production
systems with sensitive data, hire a real firm. /cso is a first pass,
not your only line of defense. Disclaimer appended to every report.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Arun Kumar Thiagarajan <arunkt.bm14@gmail.com>
Co-authored-by: Tyrone Robb <tyrone.robb@icloud.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Orkun Duman <orkun1675@gmail.com>
2026-03-22 13:19:10 -07:00
Garry TanandClaude Opus 4.6 318ffdbdf0 fix: js statement wrapping + click auto-routes option to selectOption (v0.4.5) (#117)
* fix: js statement wrapping + click auto-routes option to selectOption

Bug 1: js command wrapped all code as expressions — const, semicolons,
and multi-line code broke with SyntaxError. Added needsBlockWrapper()
and wrapForEvaluate() helpers (shared with eval) to detect statements
and use block wrapper {…} instead of expression wrapper (…).

Bug 2: clicking <option> refs hung forever because Playwright can't
.click() native select UI. Click handler now checks ARIA role + DOM
tagName and auto-routes to selectOption() via parent <select>.

Bug 3: click timeouts on <option> elements gave no guidance. Now
throws helpful error: "Use browse select instead of click."

* chore: bump version and changelog (v0.4.5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 21:50:43 -05:00
Garry TanandClaude Opus 4.6 78e519e3b7 feat: await support in browse js/eval + contributor mode v2 (#104)
* feat: support await in $B js and eval commands

Auto-wrap await expressions in async IIFE context so
$B js "await fetch(...)" works without SyntaxError.

- hasAwait() strips comments before detection
- js: expression wrapping (async()=>(expr))()
- eval: smart wrapping — single-line=expression, multi-line=block
- 6 new unit tests covering async, false-positive, and return semantics

* feat: redesign contributor mode — periodic reflection with 0-10 rating

Replace passive "report when things break" with active reflection:
- Rate gstack experience 0-10 at workflow step boundaries
- Historical calibration example (await bug) anchors the reporting bar
- "What would make this a 10" field focuses on actionable improvements
- Removed category lists in favor of judgment-based assessment

* test: add deterministic contributor mode preamble validation

40 new skill-validation tests (4 checks × 10 skills) verify:
- 0-10 rating scale present
- Calibration example present
- "What would make this a 10" field present
- Periodic reflection (not per-command)

Update existing E2E contributor eval for new report format.

* chore: bump version and changelog (v0.4.2)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: improve contributor mode + qa-quick E2E reliability

Contributor mode:
- Add "do not truncate" directive to template — agent was stopping
  after "My rating" without completing Steps/Raw output/What would
  make this a 10 sections
- Restore assertions for Steps to reproduce and Date footer

QA quick:
- Make test server URL prominent: top of prompt, explicit "already
  running" and "do NOT discover ports" instructions
- Bump session timeout 180s→240s and test timeout 240s→300s
- Set B= at top of prompt (was buried in prose)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use flexible assertions for contributor mode E2E

Agent writes thorough reports with creative section names
("Repro Steps" vs "Steps to reproduce"). Match intent not formatting:
- /repro|steps to reproduce/ for reproduction steps
- /date.*2026/ for date footer presence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add E2E eval failure blame protocol

"Not related to our changes" is an extraordinary claim that requires
extraordinary proof. When evals fail during /ship:

1. Run the same eval on main — prove it fails there too
2. If it passes on main, it IS your change — trace the blame
3. If you can't verify, say "unverified" not "pre-existing"

Added to CLAUDE.md and as a comment in skill-e2e.test.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update CONTRIBUTING.md and BROWSER.md for v0.4.2

CONTRIBUTING.md: update contributor mode description — now describes
periodic 0-10 reflection loop instead of passive friction detection.

BROWSER.md: add js/eval async documentation — await expressions are
auto-wrapped in async context, single-line eval returns values directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore v0.4.2 changelog entries lost during cherry-pick conflict

The base branch detection entries from main were dropped when resolving
the CHANGELOG conflict — should have merged both sets, not replaced.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 11:28:58 -05:00
Garry TanandClaude Opus 4.6 2aa745cb0e feat: screenshot element/region clipping (v0.3.7) (#56)
* feat: screenshot element/region clipping (--clip, --viewport, CSS/@ref)

Add element crop (CSS selector or @ref), region clip (--clip x,y,w,h),
and viewport-only (--viewport) modes to the screenshot command. Uses
Playwright's native locator.screenshot() and page.screenshot({ clip }).
Full page remains the default. Includes 10 new tests covering all modes
and error paths.

* chore: bump version and changelog (v0.3.7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add screenshot modes to BROWSER.md command reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:47:42 -07:00
Garry TanandClaude Opus 4.6 07b4e15b34 feat: v0.3.2 — project-local state, diff-aware QA, Greptile integration (#36)
* fix: cookie import picker returns JSON instead of HTML

jsonResponse() was defined at module scope but referenced `url` which
only existed as a parameter of handleCookiePickerRoute(). Every API call
crashed, the catch block also crashed, and Bun returned a default HTML
page that the frontend couldn't parse as JSON.

Thread port via corsOrigin() helper and options objects. Add route-level
tests to prevent this class of bug from shipping again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add help command to browse server

Agents that don't have SKILL.md loaded (or misread flags) had no way to
self-discover the CLI. The help command returns a formatted reference of
all commands and snapshot flags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: version-aware find-browse with META signal protocol

Agents in other workspaces found stale browse binaries that were missing
newer flags. find-browse now compares the local binary's git SHA against
origin/main via git ls-remote (4hr cache), and emits META:UPDATE_AVAILABLE
when behind. SKILL.md setup checks parse META signals and prompt the user
to update.

- New compiled binary: browse/dist/find-browse (TypeScript, testable)
- Bash shim at browse/bin/find-browse delegates to compiled binary
- .version file written at build time with git commit SHA
- Build script compiles both browse and find-browse binaries
- Graceful degradation: offline, missing .version, corrupt cache all skip check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: clean up .bun-build temp files after compile

bun build --compile leaves ~58MB temp files in the working directory.
Add rm -f .*.bun-build to the build script to clean up after each build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make help command reachable by removing it from META_COMMANDS

help was in META_COMMANDS, so it dispatched to handleMetaCommand() which
threw "Unknown meta command: help". Removing it from the set lets the
dedicated else-if handler in handleCommand() execute correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump version and changelog (v0.3.2)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add shared Greptile comment triage reference doc

Shared reference for fetching, filtering, and classifying Greptile
review comments on GitHub PRs. Used by both /review and /ship skills.
Includes parallel API fetching, suppressions check, classification
logic, reply APIs, and history file writes.

* feat: make /review and /ship Greptile-aware

/review: Step 2.5 fetches and classifies Greptile comments, Step 5
resolves them with AskUserQuestion for valid issues and false positives.

/ship: Step 3.75 triages Greptile comments between pre-landing review
and version bump. Adds Greptile Review section to PR body in Step 8.
Re-runs tests if any Greptile fixes are applied.

* feat: add Greptile batting average to /retro

Reads ~/.gstack/greptile-history.md, computes signal ratio
(valid catches vs false positives), includes in metrics table,
JSON snapshot, and Code Quality Signals narrative.

* docs: add Greptile integration section to README

Personal endorsement, two-layer review narrative, full UX walkthrough
transcript, skills table updates. Add Greptile training feedback loop
to TODO.md future ideas.

* feat: add local dev mode for testing skills from within the repo

bin/dev-setup creates .claude/skills/gstack symlink to the working tree
so Claude Code discovers skills locally. bin/dev-teardown cleans up.
DEVELOPING_GSTACK.md documents the workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: narrow gitignore to .claude/skills/ instead of all .claude/

Avoids ignoring legitimate Claude Code config like settings.json or CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: rename DEVELOPING_GSTACK.md to CONTRIBUTING.md

Rewritten as a contributor-friendly guide instead of a dry plan doc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: explain why dev-setup is needed in CONTRIBUTING.md quick start

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add browser interaction guidance to CLAUDE.md

Prevents Claude from using mcp__claude-in-chrome__* tools instead of /browse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add shared config module for project-local browse state

Centralizes path resolution (git root detection, state dir, log paths) into
config.ts. Both cli.ts and server.ts import from it, eliminating duplicated
PORT_OFFSET/BROWSE_PORT/STATE_FILE logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: rewrite port selection to use random ports

Replace CONDUCTOR_PORT magic offset and 9400-9409 scan with random port
10000-60000. Atomic state file writes, log paths from config module,
binaryVersion field for auto-restart on update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: move browse state from /tmp to project-local .gstack/

CLI now uses config module for state paths, passes BROWSE_STATE_FILE to
spawned server. Adds version mismatch auto-restart, legacy /tmp cleanup
with PID verification, and removes stale global install fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update crash log path reference to .gstack/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add config tests and update CLI lifecycle test

14 new tests for config resolution, ensureStateDir, readVersionHash,
resolveServerScript, and version mismatch detection. Remove obsolete
CONDUCTOR_PORT/BROWSE_PORT filtering from commands.test.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update BROWSER.md and TODO.md for project-local state

Replace /tmp paths with .gstack/, remove CONDUCTOR_PORT docs, document
random port selection and per-project isolation. Add server bundling TODO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update README, CHANGELOG, and CONTRIBUTING for v0.3.2

- README: replace Conductor-aware language with project-local isolation,
  add Greptile setup note
- CHANGELOG: comprehensive v0.3.2 entry with all state management changes
- CONTRIBUTING: add instructions for testing branches in other repos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add diff-aware mode to /qa — auto-tests affected pages from branch diff

When on a feature branch, /qa now reads git diff main, identifies affected
pages/routes from changed files, and tests them automatically. No URL required.
The most natural flow: write code, /ship, /qa.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update CHANGELOG for complete v0.3.2 coverage

Add missing entries: diff-aware QA mode, Greptile integration,
local dev mode, crash log path fix, README/SKILL.md updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:10:56 -07:00
Garry TanandClaude Opus 4.6 f7b95329c1 feat: Phase 3.5 — cookie import, QA testing, team retro (v0.3.1) (#29)
* Phase 2: Enhanced browser — dialog handling, upload, state checks, snapshots

- CircularBuffer O(1) ring buffer for console/network/dialog (was O(n) array+shift)
- Async buffer flush with Bun.write() (was appendFileSync)
- Dialog auto-accept/dismiss with buffer + prompt text support
- File upload command (upload <sel> <file...>)
- Element state checks (is visible/hidden/enabled/disabled/checked/editable/focused)
- Annotated screenshots with ref labels overlaid (-a flag)
- Snapshot diffing against previous snapshot (-D flag)
- Cursor-interactive element scan for non-ARIA clickables (-C flag)
- Snapshot scoping depth limit (-d N flag)
- Health check with page.evaluate + 2s timeout
- Playwright error wrapping — actionable messages for AI agents
- Fix useragent — context recreation preserves cookies/storage/URLs
- wait --networkidle / --load / --domcontentloaded flags
- console --errors filter (error + warning only)
- cookie-import <json-file> with auto-fill domain from page URL
- 166 integration tests (was ~63)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Phase 2: Rewrite SKILL.md as QA playbook + command reference

Reorient SKILL.md files from raw command reference to QA-first playbook
with 10 workflow patterns (test user flows, verify deployments, dogfood
features, responsive layouts, file upload, forms, dialogs, compare pages).
Compact command reference tables at the bottom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Phase 3: /qa skill — systematic QA testing with health scores

New /qa skill for systematic web app QA testing. Three modes:
- full: 5-10 documented issues with screenshots and repro steps
- quick: 30-second smoke test with health score
- regression: compare against saved baseline

Includes issue taxonomy (7 categories, 4 severity levels), structured
report template, health score rubric (weighted across 7 categories),
framework detection guidance (Next.js, Rails, WordPress, SPA).

Also adds browse/bin/find-browse (DRY binary discovery using git
rev-parse), .gstack/ to .gitignore, and updated TODO roadmap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Bump to v0.3.0 — Phase 2 + Phase 3 changelog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: cookie-import-browser — Chromium cookie decryption module + tests

Pure logic module for reading and decrypting cookies from macOS Chromium
browsers (Comet, Chrome, Arc, Brave, Edge). Supports v10 AES-128-CBC
encryption with macOS Keychain access, PBKDF2 key derivation, and
per-browser key caching. 18 unit tests with encrypted cookie fixtures.

* feat: cookie picker web UI + route handler

Two-panel dark-theme picker served from the browse server. Left panel
shows source browser domains with search and import buttons. Right panel
shows imported domains with trash buttons. No cookie values exposed.
6 API endpoints, importedDomains Set tracking, inline clearCookies.

* feat: wire cookie-import-browser into browse server

Add cookie-picker route dispatch (no auth, localhost-only), add
cookie-import-browser to WRITE_COMMANDS and CHAIN_WRITE, add serverPort
property to BrowserManager, add write command with two modes (picker UI
vs --domain direct import), update CLI help text.

* chore: /setup-browser-cookies skill + docs (Phase 3.5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump version and changelog (v0.3.1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: redact sensitive values from command output (PR #21)

type no longer echoes text (reports character count), cookie redacts
value with ****, header redacts Authorization/Cookie/X-API-Key/X-Auth-Token,
storage set drops value, forms redacts password fields. Prevents secrets
from persisting in LLM transcripts. 7 new tests.

Credit: fredluz (PR #21)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: path traversal prevention for screenshot/pdf/eval (PR #26)

Add validateOutputPath() for screenshot/pdf/responsive (restricts to
/tmp and cwd) and validateReadPath() for eval (blocks .. sequences and
absolute paths outside safe dirs). 7 new tests.

Credit: Jah-yee (PR #26)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: auto-install Playwright Chromium in setup (PR #22)

Setup now verifies Playwright can launch Chromium, and auto-installs
it via `bunx playwright install chromium` if missing. Exits non-zero
if build or Chromium launch fails.

Credit: AkbarDevop (PR #22)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: fix path validation bypass, CORS restriction, cookie-import path check

- startsWith('/tmp') matched '/tmpevil' — now requires trailing slash
- CORS Access-Control-Allow-Origin changed from * to http://127.0.0.1:<port>
- cookie-import now validates file paths (was missing validateReadPath)
- 3 new tests for prefix collision and cookie-import path traversal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review informational issues + add regression tests

- Add cookie-import to CHAIN_WRITE set for chain command routing
- Add path validation to snapshot -a -o output path
- Fix package.json version to match 0.3.1
- Use crypto.randomUUID() for temp DB paths (unpredictable filenames)
- Add regression tests for chain cookie-import and snapshot path validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add /qa, /setup-browser-cookies to README + update BROWSER.md

- Add /qa and /setup-browser-cookies to skills table, install/update/uninstall blurbs
- Add dedicated README sections for both new skills with usage examples
- Update demo workflow to show cookie import → QA → browse flow
- Update BROWSER.md: cookie import commands, new source files, test count (203)
- Update skill count from 6 to 8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: team-aware /retro v2.0 — per-person praise and growth opportunities

- Identify current user via git config, orient narrative as "you" vs teammates
- Add per-author metrics: commits, LOC, focus areas, commit type mix, sessions
- New "Your Week" section with personal deep-dive for whoever runs the command
- New "Team Breakdown" with per-person praise and growth opportunities
- Track AI-assisted commits via Co-Authored-By trailers
- Personal + team shipping streaks
- Tone: praise like a 1:1, growth like investment advice, never compare negatively

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add Conductor parallel sessions section to README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:31:41 -07:00
morluto a29743b056 fix: harden browse install and lifecycle checks (#4)
Thanks @morluto
2026-03-12 07:35:20 -07:00
Garry TanandClaude Opus 4.6 3d901066cd Initial release — gstack v0.0.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 01:32:16 -07:00