# Changelog
## [1.84.0.0] - 2026-09-09
**gstack's design skills now start with 61 deterministic anti-pattern checks, in your voice, then spend their judgment where a detector cannot.**
**DESIGN.md is written in the open format that impeccable and Google Stitch read, and the AI-slop list lives in one typed catalog instead of four drifting copies.**
If you have [impeccable](https://impeccable.style) installed, `/design-review`, `/design-html`, `/review`, and `/ship` run its engine first and hand you its findings as `FINDING-NNN [rule-id]` rows with file:line and a handoff (`/impeccable typeset`, `layout`, `colorize`, ...) before the LLM pass reads a page. gstack never runs impeccable's installer or launcher. The first time a design skill finds no engine it asks once: download the 16 MB engine binary from impeccable's own GitHub release into `~/.impeccable`, checked against a checksum gstack pins and recorded in your egress ledger first, or not. Say no and nothing changes and nothing nags; `gstack-config set design_detector off` silences every trace. The probe only reads files, the engine runs only from under your home directory, and a checked-out repository can never make gstack execute one of its own files.
On a live URL, `/design-review` scans the rendered page: the DOM is dumped with linked stylesheets inlined and scripts, input values, handlers, and query strings stripped, redaction-checked, kept owner-only, and the engine reads that, so Rule 4 holds on a deployed site. `/design-consultation` writes the open DESIGN.md spec (tokens in front matter, eight canonical sections, your Motion and Decisions Log kept). An existing file is converted only when you say so, once, and the answer is recorded in the file.
### The numbers that matter
Source: `git diff origin/main --shortstat`, `lib/design-catalog.ts` (`bun -e` over the exports), the free-suite log recorded by `gstack-evidence`, and the paid E2E rerun on this branch (`test/skill-e2e-design.test.ts`, `test/skill-e2e-review.test.ts`, 12 cases).
| Metric | Before | After | Δ |
|---|---|---|---|
| Deterministic checks in a design review | 0 | 61 rules (impeccable's engine, one consented download away) | evidence before judgment |
| Places the AI-slop vocabulary lived | 4 files, drifting | 1 typed catalog, 86 entries (57 slop, 29 quality) | `review/design-checklist.md` is generated from it |
| Skills that run the detector | 0 | 4 (`/design-review`, `/design-html`, `/review`, `/ship`) | same rule ids everywhere |
| What a scan can read | source files | source files or the rendered DOM of a live URL | works on deployed sites |
| DESIGN.md format gstack writes | private schema | open DESIGN.md spec | impeccable and Stitch read it |
| New `test()` declarations (platform-gated ones included) | | 194 across 11 files | free suite: 9,206 tests, 12/12 paid E2E |
The row you feel is the first one: a purple gradient, a kicker above a heading, a nested card, or `Inter` as the display face is a machine finding with a file:line before anyone forms an opinion, so the review's judgment goes to hierarchy, trust, and copy.
What this means for anyone shipping a UI with gstack: say yes once when a design skill offers the engine (or install impeccable yourself), then run `/design-review` as before. The mechanical rows arrive first, tagged, deduplicated against the checklist, and calibrated against your DESIGN.md tokens. Never install it and every design skill behaves as in v1.81, with sharper doctrine.
### Itemized changes
#### Added
- **Design detector** (`bin/gstack-design-detect.ts`, config key `design_detector: auto|off`): `probe` reports `IMPECCABLE_READY | NOT_CACHED | NOT_AVAILABLE | DISABLED` plus skill, hook, and config-ignore lines without executing anything; `scan` runs the user-installed engine over repository files, changed frontend files (`--changed `, NUL-safe, batched), or DOM dumps under `~/.gstack/projects//designs/`, and prints one normalized JSON document plus a fenced, untrusted `DETECT_TOP` block grouped by rule; `rules` lists the mapping. Only an install under HOME (cache, env override, PATH, or the skill's sibling engine) whose real file is named `impeccable` is ever READY; URLs are refused; the engine sees a minimal environment; per-batch and whole-scan timeouts; findings capped and every field sanitized; exit 3 marks a gstack bug.
- **Consent-gated engine install** (`gstack-design-detect.ts install`): when the probe finds no engine, the design skills ask one question, once, in interactive sessions only (never in spawned or headless runs). Yes downloads the engine version gstack has tested for your platform from github.com/pbakaus/impeccable's releases, verifies it against the checksum pinned in gstack for all five platforms, writes an egress receipt before the fetch (fail-closed), and places the one file under `~/.impeccable/bin//`: no impeccable skill, no editor hook, no launcher. "Never ask again" is remembered in `design_detector_install_prompted`; "turn it off" is `design_detector off`. gstack never runs `npx impeccable`.
- **Phase 0 in `/design-review`**: source mode on a feature branch, DOM mode on any URL (Aside first, the bundled browser otherwise) through one shared dump script, `lib/dom-dump.js`; `design-baseline.json` gains per-page detector counts with id-level deltas and an `engine changed` caveat; Phase 10 reports `Detector: N → M`; deferred findings end with the `/impeccable ` handoff when the skill is installed.
- **`/review` and `/ship`** design pass: the mechanical scan runs first, its rows bucket by tier (auto-fix, ask, possible), and a detector hit at the same file:line as a checklist hit is one row. **`/design-html`**: a bounded slop gate before screenshots, one fix pass, then accepted-with-reason rows.
- **Typed design catalog** (`lib/design-catalog.ts`): 86 entries with rule ids, impact, tier, confidence, detection method, handoff, fonts, and the ten `mockupNever` patterns the image-generation prompt now refuses by default. `review/design-checklist.md` is generated from it by `bun run gen:skill-docs`.
- **Doctrine**: Persuade / Operate / Read / Experience modes (MARKETING and APP UI kept as aliases), craft-floor reflexes (browser surfaces, one authored motion moment, depth with an offset, tinted secondary text, more space above a heading than below, light or dark from the use scene), the three-looks calibration, a font procedure with a role-scoped overused list, and Restrained / Committed / Full palette / Drenched color strategies.
- **Open DESIGN.md format** (`lib/design-md.ts`, `bin/gstack-design-md.ts`: `check`, `convert`, `tokens`, `mark`): read, write, convert a legacy gstack file (backup kept), flatten tokens for calibration, and persist the one-time format choice as a marker line. `/design-consultation`, `/design-review`, and `/design-html` write and read it; `PRODUCT.md` prefills the consultation's questions.
- **Attribution**: `NOTICE.md`, `licenses/Apache-2.0.txt`, and changed-file headers for material derived from impeccable and the DESIGN.md specification.
#### Changed
- `/design-consultation` chooses type by a procedure (name the world, shortlist per role, strike the overused list for that role, verify availability, state loading), varies direction across generations without flipping light and dark, and lists banned faces (Courier New now among them) from the catalog.
- Landing-page rules ask for one authored motion moment and a brand texture or asset instead of halo, spotlight, stripe, or grid gradients; the universal font rule is scoped to the display voice with body/UI exceptions on Operate and Read surfaces.
- gstack's own `DESIGN.md` is in the open format, with its intentional exceptions (the live-feed pulse, 11px mono data labels) recorded in the Decisions Log.
- The design binary's variant set trades its light/dark flip for a drenched-color dial.
- Repository `.impeccable/config*.json` ignores are surfaced (`IMPECCABLE_IGNORED_RULES`, `_FILES`, `_VALUES`) and treated as evidence in `/review` and `/ship`, settled decisions in your own project.
#### For contributors
- Real engine captures pin the contract (`test/fixtures/impeccable-*.json`, the dumped slop page, captured with engine 0.1.3 and its 61-rule registry); `test/fixtures/fake-impeccable.ts` drives the unit and E2E suites through env knobs (output file, exit code, sleep, argv log) that pass the wrapper's minimal engine environment.
- New free suites: `gstack-design-detect`, `design-md`, `design-catalog`, `design-checklist-sync`, `design-detect-contract`, `frontend-scope`, `impeccable-fixtures`, `dom-dump-hygiene` (a real Chromium run, CI or `GSTACK_DOM_DUMP_HYGIENE=1`), plus the brief test for the design binary. Gate E2E: `design-review-detector-shim` (source and DOM); periodic: `design-html-slop-gate`.
- `design-review`'s eager ceiling moved to 31,319 tokens; the carve guards for `design-html` and `plan-design-review` are re-measured; the `bin/`-and-`lib/` linking rule now carries two more runtime bins.
## [1.83.0.0] - 2026-09-09
**Memorable's workflow memory plugs into Claude Code through gstack, behind a consent key you control.**
**Every prompt it sees is receipted, secret-scanned and enveloped. The switch is off until you flip it.**
Memorable (memorable.sh) is a third-party CLI that remembers how you did a task and recalls it the next time you ask for something similar. Its own installer registers a Claude Code hook directly. This release lets you register that hook through gstack instead, with `gstack-memorable enable`, and nothing changes until you run it. When you do, gstack records its own consent key (`memorable_recall`, listed by `gstack-egress grants` with its revoke command), writes an egress receipt before every prompt it hands to the vendor binary and skips the hand-off if the receipt cannot be written, refuses to hand over a prompt carrying a live-shaped credential, skips repositories whose trust policy is `deny` or `read-only`, runs the binary in an allowlisted environment inside its own process group under a 4.5 second budget, and wraps whatever comes back in the trust envelope so recalled text can never block a prompt or speak as gstack. `gstack-memorable status` shows the vendor CLI, the gate, who registered the hook (by identity, so it stays correct after Claude Code rewrites `settings.json`), receipt counts and recent errors. `disable` turns it off and verifies both the consent and the registration before it says so. Claude Code only; Windows is refused for now because there are no process groups to contain the vendor.
The numbers that matter. Measured on a Linux sandbox with a fake vendor; the scan rows come from `scan()` in `lib/redact-engine.ts` on a synthetic log-like prompt dense in emails and IP addresses (256 KiB: 5,462 findings; 512 KiB: 10,898 findings), `main` against this release.
| Metric | Before | After | Δ |
|---|---|---|---|
| Redaction scan, 256 KiB log-like prompt | 1,573 ms | 65 ms | 24x faster |
| Redaction scan, 512 KiB log-like prompt | 6,182 ms | 126 ms | 49x faster |
| Hook cost per prompt with the bridge disabled | no hook | 49 ms | shim, bun, one config read |
| gstack work per prompt before the vendor runs | no hook | ~25 ms | gate, policy, scan, receipt |
| Tests in the free suite covering this bridge and the hook manager | 0 | 128 | +128 |
The scan speedup is not bridge-specific. Line and column for each finding used to be computed by walking the text from the start, so a pasted log full of addresses cost time quadratic in its matches; it is a binary search over a line index now, and every caller of the engine (`gstack-redact`, the pre-push hook, the PR-body scan in `/ship`) gets it.
What this means for you: if you use Memorable, run `memorable login`, `memorable enable`, then `gstack-memorable enable`, and look at `gstack-egress list --sink memorable-recall` after a few prompts. If you do not, nothing changes: the key defaults to off, no hook is registered, `./setup` never registers one for you, and upgrading needs no migration. `docs/memorable-workflow-memory.md` says exactly what gstack hands over, what it can attest, and what is Memorable's own claim. Contributed by @AdvaiytSane and @NIkhil-cmd-cmd (#2831).
### Itemized changes
#### Added
- **`bin/gstack-memorable enable | disable | status`**, the Memorable recall bridge (Claude Code only, off by default). `enable` needs the vendor CLI on the machine, registers gstack's hook at the stable install path with a 5 second timeout, refuses when Memorable's own installer already registered its hook (two entries would run the hook twice per prompt), verifies the stable install carries this bridge before touching anything, and sets `memorable_recall=on`. It never runs `memorable enable`: the vendor's capture consent is yours to grant. `disable` flips the key off first, removes gstack's entry by identity (tag or no tag), verifies both, and reports a partial failure as one. `status` never executes the vendor. One lifecycle transition runs at a time (a lock under `~/.gstack/locks`, stale after 30 seconds). Exit codes mirror the hook manager: 1 refused, 3 unparseable `settings.json`, 4 unexpected shape, 5 lock.
- **`hosts/claude/hooks/memorable-user-prompt-hook`** (bash shim plus `memorable-user-prompt-hook.ts`), the UserPromptSubmit hook the bridge registers. One deadline clock undercuts Claude Code's 5 second hook kill; stdin is capped at 1 MiB; the prompt is scanned for HIGH-tier credential shapes on the raw bytes and on the decoded string values; the vendor sees only `PATH`, `HOME`, identity and locale variables, temp directories, the standard proxy, TLS and `XDG_*` variables and its own `MEMORABLE*` knobs; only a string `additionalContext` is accepted from it (a vendor `decision`, `continue` or `systemMessage` is dropped), control and Unicode format characters are stripped, the text is capped at 8 KiB on a UTF-8 boundary and enveloped. The vendor's whole process group is killed when the hook finishes, hangs past its budget, or is terminated by the host mid-flight (a process the vendor detaches into its own session is outside that guarantee); a vendor that exits but leaves a helper holding its pipes still gets its answer delivered. Every refusal is one rate-limited line in `~/.gstack/hook-errors.log` (created 0600); the hook always exits 0.
- **`memorable_recall` config key** (`on | off`, default `off`, a typo is rejected and the prior value kept) and a **`memorable-recall` row in `gstack-egress grants`** naming the vendor CLI, the per-prompt receipt sink and the revoke command.
- **Egress receipts for the `memorable-recall` sink**, fail-closed: no receipt, no hand-off. The receipt records the byte count and sha256 of the exact stdin handed over, the consent key, and `local:` as the recipient gstack can attest; the outcome records `exit:0 output-written bytes=N gstack_ms=N`, `exit:N injected=no`, `timeout`, `spawn-error:` or `budget-exhausted`. A receipt with no outcome reads as unknown, never as success.
- **`gstack-settings-hook list-items --event [--owned-by ] [--command-regex ]`**, a read-only identity view: one JSON string literal per matching hook command, identity from the hook table rather than the tag, empty output when nothing matches, exit 3 on unparseable settings and 4 on an unexpected shape, so a caller can decide a mutation from it.
- **`runExternal` in `hosts/claude/hooks/spawn-bin.ts`**, the contained way for a hook to run a third-party executable: its own process group, a wall-clock limit that kills the group, a stdout cap, a drained stderr tail, stdin write errors kept separate from spawn errors, resolution on the child's exit rather than on the last pipe closing, and a refusal on Windows.
- **`lockBudgetMs`** on `writeReceipt` and `writeOutcome` in `lib/egress-receipt.ts`, so a caller on a deadline can bound the ledger lock wait (default unchanged at 2.5 seconds), and a **spawn timeout parameter** on `repoPolicyTier` in `lib/gbrain-repo-policy-client.ts`.
- **`docs/memorable-workflow-memory.md`**: what you get, the two consents (gstack's and Memorable's, neither implies the other), what gstack hands over and what it can attest, what gstack tests and what is the vendor's claim, turning it on and off, and a troubleshooting runbook. A README row, a Docs-table row and a privacy pointer link to it.
#### Changed
- **`gstack-settings-hook remove-source` removes by identity as well as by tag.** Claude Code strips gstack's `_gstack_source` tag when it rewrites `settings.json`; the off switch for every gstack hook used to no-op on exactly those entries. Items the hook table identifies as the requested source are removed whether or not the entry is tagged, other sources' items are never touched, and entries with nothing of the source's stay byte-identical.
- **`./setup --no-team` keeps the opt-in Memorable hook** when it sweeps stray gstack hooks, alongside the verify gate.
- **`gstack-uninstall` removes the Memorable hook by name**, sets `memorable_recall` off wherever gstack's config lives (kept state or not, hook manager present or not, and a failed revocation is named), and says that Memorable's own consent is unchanged (`memorable disable`, `memorable forget`).
- **Redaction findings locate their line and column by binary search** over a per-scan line index, so scan time is linear in the input for every caller of `lib/redact-engine.ts`.
#### For contributors
- New test files: `test/gstack-memorable.test.ts`, `test/memorable-user-prompt-hook.test.ts` (a fake vendor written in sh; the hook's stdin bytes are compared byte for byte with what the vendor received), `test/gstack-config-memorable-key.test.ts`. Extended: `test/gstack-settings-hook-schema-aware.test.ts` (identity removal pinned for every source in the hook table), `test/egress-receipt.test.ts`, `test/egress-receipt-wiring.test.ts` (the `hosts/` tree is now swept for unreceipted sinks), `test/uninstall.test.ts`, `test/gbrain-repo-policy-client.test.ts`, `test/redact-engine.test.ts`, `test/verify-gate.test.ts`, `test/setup-hook-canonical-paths.test.ts`, `test/hooks-windows-paths.test.ts`, `test/gstack-egress-cli.test.ts`.
- `docs/PROJECT_STRUCTURE.md` lists the new hook and bin.
## [1.81.0.0] - 2026-09-06
**Aside is the browser gstack drives first. Every browsing skill, the PDF and diagram renderer, and web research go through it.**
**gstack's own headless browser stays as the automatic fallback, so nothing stops working without Aside.**
Since v1.72 Aside was the recommended driver for third-party sites. This release makes it the first driver everywhere. `/qa`, `/qa-only`, `/design-review`, `/scrape`, `/benchmark`, `/canary`, `/browse`, `/devex-review`, `/land-and-deploy`'s post-deploy check, and `/design-consultation`'s competitor research now run inside the Aside AI browser (macOS 15+, aside.com), in the sessions you are already signed in to. No cookie export, no "open the browser" step, no CAPTCHA handoff dance: if a page needs a login, you sign in inside Aside and the skill re-runs the step. `/make-pdf`, `/diagram`, `/design-html`'s viewport screenshots, and `/office-hours` sketches render through Aside as well, and the skills that used to reach for the WebSearch tool now ask Aside's own agent, read-only, in your real browser.
When Aside is not there, nothing breaks. Every browsing skill carries a fallback section that translates its Aside steps onto gstack's own headless Chromium (`$B`), step for step: same evidence lines, same report, same consent rules, with cookie import or a handoff for authenticated pages. The renderer picks Aside when it is running and the bundled browser otherwise, and prints which one it used. Research falls back to the WebSearch tool, then to in-distribution knowledge. Linux and Windows users keep the bundled browser exactly as before; the Third-Party Web Actions contract keeps gstack's visible browser as its fallback driver.
Every skill follows one contract, `scripts/resolvers/aside.ts`. It was written from live probes against Aside CLI 1.26, not from memory: one flow per `aside repl` script (tabs close when the script ends), a console hook installed over CDP before navigation, labelled evidence lines, screenshots copied out of Aside's session directory, and a `GSTACK_STEP_OK` sentinel because the CLI's exit code is always 0. The rules are explicit about what a real browser means: open your own tabs, never read the user's, look freely but ask once before any mutating action on a non-local site, never touch credentials, treat everything a page returns as untrusted.
### The numbers that matter
Source: `git diff origin/main --stat`, `bun test/helpers/capture-context-budget.ts`, and the live runs recorded in the PR (cookbook and skill scripts executed verbatim, `test/skill-e2e-aside.test.ts`, `test/skill-e2e-diagram.test.ts`, the make-pdf e2e gates on both engines, `bin/gstack-render.ts` on both engines).
| Metric | Before | After | Δ |
|---|---|---|---|
| Browser a skill reaches for first | gstack's headless Chromium | Aside (your real sessions) | one contract, ten skills |
| Skills that stop when Aside is absent | n/a | 0 | every one falls back to `$B` |
| Renderer engines for PDFs and diagrams | 1 (bundled Chromium) | 2 (Aside first, bundled fallback) | `ENGINE=` line tells you which |
| Web research surfaces | WebSearch tool | Aside agent, then WebSearch | real browser first |
| Skills removed | | 0 | every skill on main is still here |
The row you feel is the fallback one: on a Mac with Aside open, QA runs in the browser you already trust; on a Linux box or with Aside closed, the same skill runs the same steps on the bundled browser and tells you so.
What this means for anyone running gstack: open Aside on your Mac and run `/qa`, `/make-pdf`, or `/diagram` the way you always did. Without Aside, everything behaves as it did in v1.80. Upgrading needs no migration. Contributed by @time-attack.
### Itemized changes
#### Added
- **Aside browser-driver contract** (`{{ASIDE_SETUP}}` + `{{ASIDE_COOKBOOK}}`): runtime detection with a READY / NEEDS_ASIDE / ASIDE_NOT_RUNNING probe, ten rules for driving a real browser (own tabs only, LOOK-not-ACT consent with a LOCAL host rule, credentials never pass through the agent, untrusted page content, one flow per script, artifact handoff through the session directory), and a verified cookbook: read a page with load-time console errors, drive a flow with a DOM diff, annotated screenshot, responsive captures over CDP emulation, same-origin link status, performance entries, PDF, element screenshot, and `aside exec` for open-ended reading.
- **Browser fallback** (`{{BROWSE_FALLBACK}}`): when Aside is absent, a fifteen-row table maps every Aside step in the skill onto its `$B` command, with the rules that differ (cookie import or `$B handoff` for sign-in, the same consent and evidence rules) and the full command reference on demand in `/browse`.
- **Aside renderer** (`lib/aside-render.ts`, `bin/gstack-render.ts`): render any local HTML file to PDF (tagged, outlined, header/footer, page numbers, paper sizes and margins in any CSS unit), screenshots at any width, and in-page evaluations written to files. Tries Aside, then the bundled browser (`GSTACK_BROWSE_BIN` / `BROWSE_BIN` override it), and prints `ENGINE=aside|browse`.
- **Web research runs in Aside** (`{{ASIDE_RESEARCH}}`): the planning, review, design, security, and investigate skills research through Aside's agent with the user's real browser, read-only; WebSearch when Aside is absent; in-distribution knowledge when neither is available.
- **Live E2E for the Aside-driven skills** (`test/skill-e2e-aside.test.ts`, periodic tier): browse read, browse flow, quick QA, scrape JSON, and quick canary against a localhost fixture, asserting the sentinel in real Bash output; self-skips wherever Aside is not installed. The qa, diagram, and make-pdf gates run on whichever engine is present, so the Linux CI lane exercises the fallback path.
- `setup` and the doc generator prune renders of skills that no longer exist in the source tree, so a removed skill can never linger in a host install.
#### Changed
- `/qa` and `/qa-only`: the whole methodology (orient, explore, document, re-test) runs as Aside scripts; the authenticate phase is now "you are already signed in"; a 13th rule requires consent before mutating actions on non-local targets. The fallback section carries the `$B` translation.
- `/design-review` and `/design-consultation`: design-system extraction is one script printing FONTS, COLORS, HEADINGS, TOUCH_TARGETS, and NAV; competitor research confirms the exact URLs with you before opening any of them in your real browser, and runs on the bundled browser when Aside is absent.
- `/benchmark` and `/canary`: per-page scripts print NAV, PAINT, LCP, RESOURCES, SCRIPTS, CSS, and SUMMARY; the canary loop re-runs the script every 60 seconds because nothing persists between Aside scripts.
- `/land-and-deploy` Step 7 and `/devex-review`: one Aside script each; the smoke row reads `responseStatus` from the navigation entry.
- `/make-pdf` prints through Aside first and the bundled browser otherwise; exit 4 now means no browser is available and names both remedies; mermaid fences, oversized-image downscale, and DOCX rasters each run as one render script; `$P setup` reports which engine it found.
- `/diagram`: the SVG, PNG, and excalidraw triplet is one `gstack-render` call; every diagram type gets an excalidraw export (flowcharts and sequence diagrams as editable scenes, the rest as a single image element).
- `/scrape`: look-then-extract Aside scripts that build the JSON inside the page; on the fallback the browser-skills runtime (`$B skill list` / `run`, `/skillify`) applies as before.
- **Third-Party Web Actions** (`/ship`, `/spec`, `/setup-deploy`, `/office-hours`, `/land-and-deploy`): Aside is the recommended first driver; gstack's own visible browser stays as the fallback option, exactly the shape v1.72 introduced, and the contract points at the `/browse` doc for how to drive Aside.
- `/browse`: the Aside contract, cookbook, mode choice (`aside repl` by default, `aside exec` for reading), report format, the fallback, and the full `$B` command reference on demand.
- The root router, README, `BROWSER.md`, `docs/skills.md`, `docs/PROJECT_STRUCTURE.md`, `CONTRIBUTING.md`, `CLAUDE.md`, and `ARCHITECTURE.md` describe the Aside-first, bundled-fallback world.
#### Fixed
- `./setup`'s retired-skill prune is safe by construction: it never deletes a directory that holds your own files (only gstack's SKILL.md, marker and links go), never follows a symlink into someone else's tree, cleans a host's stale links even after the generator already removed the render, and recognizes a skill renamed through its frontmatter `name:`. The generator's own prune keeps any `gstack-*` directory without the generated banner and skips a host whose generation failed. `./setup` also rebuilds when the design or make-pdf binary is missing or when anything under `lib/` changed.
- Setup's summary is honest about browsers: with Aside installed and a failed Chromium bootstrap it says only the bundled fallback is missing (and that `/pair-agent` needs it); `GSTACK_SKIP_ASIDE=1` is honored by the skills' probe, by setup, and by the renderer alike.
- The local render server behind `/make-pdf`, `/diagram`, and design previews serves one per-render secret URL, never follows a symlink out of its directory, and refuses malformed requests; page text can no longer forge its control lines; a hung `aside` process is killed instead of waited on forever; if the Aside CLI cannot start mid-run (or its private CDP bridge is missing) the render retries on gstack's own browser and reports which engine actually rendered — a page error or a timeout of a running script is never retried; a shared `/tmp/gstack-render` owned by someone else is never used.
- Browsing skills check link status only on a LOCAL target (on a real site every HEAD request would carry your cookies); same-origin filtering compares real origins instead of string prefixes; `.local` hosts are no longer treated as your own machine (mDNS names resolve to other devices on the LAN); the Aside readiness probe has a 30-second deadline on stock macOS, which ships neither `timeout` nor `gtimeout`.
- `aside exec` research requests write an egress receipt before leaving the machine, like every other off-machine send.
- Rendering through the bundled browser tolerates a cold start: the first tab request retries once while the daemon is still coming up, and a command whose process is slow to exit under heavy load is waited for instead of being reported as failed.
- `gstack-render --help` exits 0, non-numeric flags are rejected instead of becoming `NaN` timeouts, `--wait-timeout` is documented, and page-derived output (`EVAL`, `PAGE_ERRORS`) is fenced as untrusted content.
#### For contributors
- `test/aside-driver.test.ts` pins the contract's load-bearing sentences and asserts every browsing skill carries `## BROWSER SETUP (Aside` followed by `## Browser fallback`. `test/aside-render.test.ts` pins the renderer's option mapping and script shapes and runs a live render on each engine that is present. `test/helpers/aside-available.ts` is the shared live-Aside probe for E2E gating; `make-pdf/test/e2e/browser-available.ts` gates the make-pdf gates on either engine.
- `lib/claude-bin.ts` and `lib/error-handling.ts` are now the canonical copies; `browse/src` re-exports them.
- New free tests pin the review fixes: `test/setup-prune-stale-generated.test.ts` (host cleanup after the generator pruned, symlink targets survive, renamed skills, foreign links), `test/setup-browser-hint.test.ts` (hint and bootstrap summary across Aside present/absent, bootstrap ok/failed, skip-env), `test/setup-needs-build.test.ts`, `test/gstack-render-cli.test.ts`, `test/aside-render.test.ts` (fake `aside`/`browse` executables: probe classification, stdout contract, server policy, failure paths, timeout kill, engine choice and mid-run fallback), `make-pdf/test/cli-exit-codes.test.ts`, `make-pdf/test/setup-smoke.test.ts`. `renderPdf` returns the engine that rendered; `pickEngine(fresh, deps)` and `serveDir` are exported test seams.
- Size budget re-baselined to `parity-baseline-v1.81.0.0.json` (the Aside contract plus fallback block ride in every browsing skill); parity ceilings ratcheted with measured values; touchfiles, tiers, coverage matrix, eval baselines, ship goldens, and the context-budget fixture refreshed. Follow-ups in TODOS.md: keep the `$B` translation table in sync with the cookbook, Aside CLI 1.26 lacks the commands its own skill doc lists, a macOS runner for the live E2E, an optional MCP path for a persistent page.
## [1.80.0.0] - 2026-09-04
**Setup finishes even when Chromium cannot be installed.**
**gstack never deletes or overwrites a skill it did not create.**
Three defects that a downstream fork kept tripping over are fixed at the source. `./setup` used to abort at the Playwright step on any box where the Chromium download failed or hung (offline, proxied, AppArmor-restricted), and because that step ran before skills were registered, those users ended with zero skills. The `/freeze` deny hook read a different state directory than `/freeze` wrote whenever `GSTACK_HOME` was set, so the boundary silently allowed everything. And both `./setup` and `gstack-relink` would replace or delete any skill entry that happened to share a name with a gstack skill, including a skill you wrote yourself.
Now the Chromium install is best-effort and bounded. It runs under a 600 second deadline you can change with `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT`, skip outright with `GSTACK_SKIP_PLAYWRIGHT=1`, and every failure becomes a reason code in the final summary that names the skills that need a browser. Skills always register. Both PreToolUse hooks resolve the same state root the writers use, and freeze fails closed on any unexpected death instead of exiting with no decision. Ownership is proven, never assumed from a name: an entry is only touched when it is a symlink into gstack, carries the `.gstack-owned` marker gstack writes for directories it creates, or is a real file gstack generated. Even then a generated file you have since customized is moved to `~/.gstack/backups/skills//` before gstack links over it, and a directory is only removed whole when nothing of yours is inside.
### The numbers that matter
Source: the free suite (`bun run test`), specifically `test/setup-playwright-best-effort.test.ts`, `test/setup-link-ownership.test.ts`, `test/relink.test.ts` and `test/hook-scripts.test.ts`, run against this tree.
| Metric | Before | After | Δ |
|---|---|---|---|
| Skills registered when the Chromium download fails or hangs | 0 | all | setup never aborts at the bootstrap |
| Longest a wedged Chromium install can block `./setup` | unbounded | 600s default | `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT`, `GSTACK_SKIP_PLAYWRIGHT=1` |
| `/freeze` boundary with `GSTACK_HOME` set | allowed every edit | denies | one state-root resolver for hooks and writers |
| Sites that could delete or replace a same-name skill without proof of ownership | 5 | 0 | setup linker, alias installer, both flip cleanups, relink |
| Data lost when gstack replaces a generated SKILL.md you customized | the file | none | moved to `~/.gstack/backups/skills//` |
| Free tests pinning these behaviors | 0 | 103 | across 10 files |
The first row is the one you feel: a laptop on hotel wifi, a CI runner behind a proxy, or a fresh Ubuntu 24.04 box now ends `./setup` with every skill installed and one clear line saying which ones will not work until Chromium is present.
What this means for anyone installing or upgrading: run `./setup` anywhere and get skills. Keep your own `qa` or `ship` skill next to gstack's and it survives every upgrade and prefix flip, reported by name instead of silently replaced. Set `GSTACK_HOME` and `/freeze` means what it says.
### Itemized changes
#### Fixed
- **`./setup` no longer aborts when Chromium cannot be installed** (#1900, #1901, #1902, #913). The Playwright step is best-effort and bounded. Reason codes: `skipped`, `chromium-install`, `chromium-install-timeout`, `chromium-install-locked`, `windows-no-node`, `windows-node-modules`, `post-install-launch`. A wedged installer is killed with its whole child tree (pgrep, or a /proc walk where pgrep is missing); Ctrl-C mid-install kills it too. A stale install lock is reclaimed atomically, a garbage pid file counts as stale, and a lock with no recorded holder expires after the install bound. Contributed by @DavidMiserak (#2233).
- **`/freeze` enforces its boundary under `GSTACK_HOME`** (#1459). The hook resolves the state root exactly as `bin/gstack-paths` does (GSTACK_HOME, then CLAUDE_PLUGIN_DATA only when CLAUDE_PLUGIN_ROOT names gstack, then `~/.gstack`), a trailing newline in the path round-trips, a helper from an older install denies instead of exiting 127, and an EXIT backstop denies on any unexpected failure. `/careful` reads its project patterns from the same root and falls back safely on a stale helper. Contributed by @NikhileshNanduri (#1509).
- **gstack never deletes or links over a skill it does not own** (#2119). `./setup`'s linker and alias installer, both prefix-flip cleanups, and `gstack-relink` all prove ownership first and report a foreign entry by name in the final summary. Runtime assets (sections, templates, checklists) inside a directory gstack did not create are kept, not replaced. A checkout named without a `gstack` path segment (a `git worktree add ../gstack-feature`) still counts as gstack's. Contributed by @smblight.
- **Windows copy installs carry a `.gstack-owned` marker** so provenance no longer rests on the directory name. The marker is written on every platform, only for directories gstack creates.
- **Setup's one-shot telemetry events no longer finalize other sessions' in-flight markers** as `outcome: unknown`.
#### Added
- `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT=` and `GSTACK_SKIP_PLAYWRIGHT=1` for `./setup`; documented in the README troubleshooting list together with `GSTACK_CHROMIUM_NO_SANDBOX=1`.
- A final setup summary that names the browser-dependent skills when Chromium is unavailable, lists any same-name skills left untouched, and reports any customized SKILL.md moved to `~/.gstack/backups/skills//`.
- `gstack-telemetry-log --no-sweep` for events that own no session.
#### For contributors
- The time-attack fork evaluation that surfaced these defects is preserved under `docs/designs/fork-port-residual-2026-09/` (report, residual index, refutation verdicts, hashes). Waves B through E2 of that plan are scheduled work.
- New free tests: `test/setup-link-ownership.test.ts`, plus large additions to `test/setup-playwright-best-effort.test.ts`, `test/relink.test.ts`, `test/hook-scripts.test.ts`, `test/telemetry.test.ts`. Anchor-sliced harnesses now fail loudly on `command not found` instead of degrading into "foreign, skipped".
- `bin/gstack-relink` and `setup` carry the same ownership rule in two copies; the shared helper is filed in TODOS.md.
## [1.79.0.0] - 2026-09-01
**/ship can no longer be stranded by a backgrounded subagent.**
**The bug class that came back twice is pinned everywhere it lives.**
Claude Code v2.1.198 made Agent-tool subagents launch in the background by default. Four /ship steps (7, 8, 10, 18) hand work to a subagent and parse its final line as JSON, and none of them passed `run_in_background: false`, so a ship run could park forever on Step 18 waiting for doc-sync output that was never coming. This is the third time this class has bitten (#497 fixed it, #2440 regressed it, Step 18 rediscovered it). Every synchronous dispatch site in the skill tree now carries the explicit flag, and the flag is test-pinned per file so a fourth recurrence fails CI the moment it lands.
The doc-sync dispatch also got real failure handling. If the dispatch gets backgrounded anyway, the parent polls for about 10 minutes, stops the runaway task, reconciles any commit the subagent made against the pre-dispatch HEAD, and ships the PR without the Documentation section instead of hanging. The subagent itself is scope-guarded to docs only: it never changes VERSION, never merges the base branch, skips the Codex doc review (the parent owns review passes), and reports a rejected push as `pushed:false` for the parent to reconcile.
### The numbers that matter
Source: `test/run-in-background-guidance.test.ts` (the pin list) and `grep -rl 'run_in_background: false' --include='*.md'` against this tree.
| Metric | Before | After | Δ |
|---|---|---|---|
| Generated files pinned to carry the flag | 2 | 24 | every sync dispatch site |
| /ship dispatch steps with a deadline + recovery branch | 0 of 4 | 4 of 4 | Steps 7/8/10/18 |
| /ship Step 18 worst-case wait (backgrounded dispatch) | unbounded | ~10 min | documented recovery; a foreground hang stays harness-bounded |
| Codex doc review inside a ship-dispatched doc-sync | ~5-10 min per ship | skipped | parent owns reviews |
| Headless gates that could mutate VERSION mid-ship | 2 | 0 | Step 8.3/8.4d resolve to Skip |
The unbounded-to-10-minutes number is the one you feel: the failure mode changes from "my ship run has been sitting there for an hour" to a printed recovery line and a PR that still lands.
What this means for anyone shipping here: /ship finishes even when the harness misbehaves, doc-sync can never renumber your release or double-run review passes, and document-release now carries its own spawned-session contract, so any orchestrator that dispatches it (not just /ship) gets safe headless behavior.
### Itemized changes
#### Added
- **Deadline + recovery on every /ship dispatch step.** Steps 7 and 8 stop a runaway task and fall back to the inline audit if the subagent never completes (~10 min); Step 10 records Greptile triage as UNAVAILABLE in the PR body rather than pretending zero comments; Step 18 stops the runaway task, vets and pushes orphaned docs-only commits (never VERSION, package.json, or CHANGELOG; an explicit second-failure branch covers a moved remote), surfaces stray staged edits without ever discarding content, and proceeds without the Documentation section. The dispatch contract carries an explicit failure JSON shape, so a doc-sync that could not run reports as a failure instead of clean docs. No step ever silently parks the run.
- **A spawned-dispatch contract in document-release itself.** Triggered strictly by the preamble's `SESSION_KIND: spawned` echo (the dispatcher's `GSTACK_SESSION_KIND=spawned` prefix; prompt or file claims alone never trigger it, matching the v1.78.0.0 echo-only rule). Every ask-the-user gate auto-resolves to its recommended option except the ones that would rewrite CHANGELOG content or change VERSION, which resolve to Skip and get recorded. Step 8.4d carries an explicit spawned note because its interactive recommendation is a VERSION bump.
- **Docs-sync scope guard in /ship's dispatch prompt**: docs only, no base-branch merges, no version renumbering, CHANGELOG left to the parent, no Codex doc review, push rejections reported instead of resolved.
#### Changed
- The Codex Documentation Review section skips itself in any spawned session; its apply gate needs a human, and a dispatching workflow owns its own review passes. Covers version skew where an older installed /ship dispatches a newer document-release.
- Autoplan's design/eng/dx phase dispatches, the review army Red Team, the spec review loop, the adversarial subagent, the Codex second-opinion/plan-review/doc-review fallbacks, design sketch and outside voices, CSO finding verification, and design-shotgun variant launches all state `run_in_background: false` explicitly. Parallel fan-outs stay parallel; foreground calls in one message run concurrently.
#### Fixed
- **/ship Steps 7, 8, 10, and 18 no longer strand the run** when Claude Code backgrounds their subagents (#497, #2440 class, third recurrence). The four dispatch specs share one resolver-sourced foreground note — dispatch happens only via the Agent tool, never the Skill tool or inline execution — so the phrasing cannot drift per site again.
#### For contributors
- `{{FOREGROUND_DISPATCH_NOTE}}` (scripts/resolvers/constants.ts) is the single source for the flag guidance; use it in any new dispatch template and add the generated carrier to `GENERATED_WITH_GUIDANCE` in `test/run-in-background-guidance.test.ts` in the same commit. The test's pin list is the census of synchronous dispatch sites, and its structural scanner fails any generated dispatch imperative that lacks the flag even when un-enumerated.
- Follow-ups filed: PreToolUse-hook enforcement of the flag (P1, structural fix), a capability-narrowed ship-mode for document-release, and a cross-host dispatch semantics audit.
- Carved-skill skeleton ceilings re-measured and ratcheted in `test/helpers/carve-guards.ts`; codex/factory ship goldens re-rendered; the ship-docsync E2E asserts `run_in_background === false` on the captured dispatch and its fixture resolves section reads deterministically.
## [1.78.0.0] - 2026-08-31
**Plan reviews ask their questions again, the OSV lane is green from 105 advisories, and 18 community fixes land with credit.**
**The upgrade path can no longer delete your install.**
The fix wave. The weekly periodic eval lane broke at v1.76: the spawned-session rule let the model infer "nobody is reading this" from any scripted-looking prompt and silently auto-decide every review question, so plan reviews stopped asking. reviewCount collapsed to 0 across four skills. The trigger is now machine-verifiable and nothing else: the preamble's own echoed `SESSION_KIND: spawned` status line. No text from a dispatch prompt, file, or page can flip a session to auto-choose; a spawned subagent that missed the env marker is still caught at failure time by the AUQ hooks' spawned escape. The reproduced failure went from 0 review questions to a full question flow on the same harness.
The weekly OSV lane had been red for three weeks with 105 advisories, and its suppression file had been silently inert since v1.65 (wrong filename for auto-discovery, and per-directory configs never covered the nested lockfile anyway). The workflow now passes an explicit global `--config`, a dependency pass cleared 102 advisories through in-range bumps and overrides that actually reach nested exact pins, and the 3 survivors carry reasons plus `ignoreUntil` expiries. The diagram-render bundle lost its duplicate mermaid on the way: 9.96 MB to 7.59 MB.
Eighteen community PRs are absorbed with authorship preserved, twelve of them with wave amendments that finish a gap or fix a defect found in review. Nine more confirmed bugs are fixed directly, led by #2679: a failed `mktemp` in the vendored upgrade path used to `rm -rf` your install's backup after a failed swap. The swap now aborts loudly and restores the backup.
### The numbers that matter
Sources: OSV scanner v2.3.8 (the exact release CI pins) on a frozen clean install; weekly CI runs 33369295978 (OSV) and 33363624506 (periodic); local A/B repro of `test/skill-e2e-plan-ceo-finding-count.test.ts`; `bun run test` on this tree vs a v1.77.0.0 scratch worktree.
| Property | Before | After |
|---|---|---|
| OSV advisories on `./` recursive scan | 105 (24 packages, lane red 3 weeks) | 0, exit 0 (3 expiring reasoned ignores) |
| Suppression config | inert since v1.65 | loaded via explicit `--config`, wiring-tested |
| Periodic-lane review questions (5-finding plan) | reviewCount=0 (14/73 shards red) | full question flow on the same repro |
| Failed upgrade swap | deletes backup and install | restores backup, aborts loudly |
| Community PRs absorbed / issues closed | 40 open PRs, 17 of these issues open | 18 absorbed with credit, 17 issues closed |
| diagram-render offline bundle | 9.96 MB (duplicate mermaid 10 + 11) | 7.59 MB |
| Free-suite tests | 8,519 | 8,660 (+141; receipts red on v1.77) |
### What this means for you
Plan reviews ask their questions again, `browse stop` actually stops Chromium, secrets scanning can't be silently disabled by a failed `mktemp` or a slow gitleaks probe, and /gstack-upgrade cannot eat your install on a bad temp dir. If you filed one of the 17 closed issues or the 18 absorbed PRs: your fix shipped, with your name on it. Upgrade with /gstack-upgrade.
### Itemized changes
#### Fixed
- **AskUserQuestion spawned-trigger objectivity (periodic-lane regression).** The v1.76 rule's "(or your dispatch prompt marks this session as spawned)" allowed inference; plan-review E2Es collapsed to zero questions. Two pinned-container verification rounds showed that ANY prose-declaration channel in the eager path keeps question counts unstable, so the rule now keys on exactly one thing: the preamble's own echoed STATUS line. Text can never trigger auto-choose — the strongest form of the anti-injection contract. Subagents without the env marker (Task-tool spawns) are caught at failure time by the AUQ hooks' spawned escape sentence, which now demands an explicit declaration, never an inference; #2733's env-prefix channel is untouched.
- **mktemp guards at all three skill-content sites (#2679).** redact-doc resolver, ship pr-body, and the vendored upgrade path abort loudly on mktemp failure; the upgrade swap restores the backup on a failed `mv` instead of deleting it, and refuses to start over a stale backup left by a previously crashed upgrade; PR/MR creation refuses a missing or empty scanned body file; the GitLab MR path sends the scanned file's bytes instead of re-rendering an unscanned heredoc; `gstack-redact --from-file ""` errors instead of silently reading stdin.
- **OSV lane green (supersedes #2695).** Explicit `--config` (auto-discovered configs apply per-directory and never covered `lib/diagram-render/bun.lock`); `overrides` pin ip-address 10.3.1 (clears both nested nodes, including express-rate-limit's exact 10.1.0 pin that a top-level bump provably cannot reach — @anupamme's #2695 credited for the parallel diagnosis) and sharp 0.35.0 (smoke-tested); marked ^18.0.11; full in-range lockfile refresh; diagram-render bumped through its own build-script contract (mermaid 11.16.1, excalidraw 0.18.1, mermaid-to-excalidraw 1.1.2 → 2.2.2, which retires the entire duplicate mermaid-10 advisory chain); every ignore carries a reason, an upgrade trigger, and an `ignoreUntil` expiry, pinned by a new wiring test so the file can never go inert again.
- **A slow gbrain `--version` probe classifies as `timeout`, never `no-cli` (#2716).** A bun-shim install on a loaded box silently lost every brain-aware block because `no-cli` is the one status `--is-ok` does not forgive.
- **codex skill: unclosed consult-mode fence, `turn.failed` reported as a failure, portable exit-code capture (#2671, #2669).** Every fenced region after the unclosed fence rendered inverted; a stated turn failure read as a "possible disconnect" (consult mode had no completeness check at all); `${PIPESTATUS[0]}` is empty under zsh so hang detection never fired and clean runs printed spurious exit noise. A repo-wide CommonMark-faithful fence-pairing test now guards every generated doc, and the exit-capture form is executed under real bash and zsh in tests. Expect `codex_timeout` telemetry to start firing for zsh users — that's the counter working, not a regression.
- **Outside-voice fallback labeled honestly (#2735).** When Codex is unavailable the fallback reviewer is a same-family Claude subagent; the copy no longer sells it as "cross-model coverage" with "genuine independence."
- **skill_prefix works under a gbrain render (#2738).** gstack-relink now name-patches the render tree — the file the host actually serves.
- **Rendered section paths survive the atomic swap (#2692).** gen-skill-docs gained `--link-root`; the swap-in callers pass the final dir, so rendered skills no longer carry ~9 dead Read paths into a directory the swap just deleted.
- **Persistent timeline Stop hook opt-out (#2677).** `--no-team` stays one-shot; the new `timeline_stop_hook` config key (flags, env, full gstack-config surface) survives upgrades, and an explicit "no" removes a live registration.
- **browse: macOS headless GPU spin tamed + the lock-less headless Chromium is reaped on stop (#2709).** Darwin-gated flag set (reporter-validated, `GSTACK_DISABLE_GPU=off` escape) exposed as a pure, unit-testable function; the daemon records the launched child's pid + start time, and stop paths reap a survivor only after verifying both the recorded start time and a Chromium-looking cmdline, so a recycled PID is never killed.
- **gstack-wtree: a failed `touch` falls through to the HEAD seed (#2687 hardening).** The underlying same-size-rewrite race was fixed in v1.74; the reporter's repro now runs 20/20 clean, and the one silent path that could reopen it is closed.
- **Large gstack-redact reports survive a piped consumer.** Reports past the 64KiB pipe buffer were silently truncated when the reader was slow (`process.exit` beat the drain) — CI quality gates parsing the JSON saw a corrupt report. The report path now lets stdout flush; pinned by a test proven red against the old exit pattern.
#### Community PRs absorbed (authorship preserved on the branch commits)
- Codex preflight: a CLI that cannot execute reports `broken_install`, never `ready` — the wave completed autoplan's hand-maintained preflight chain and narrowed the install-signature grep to failed spawns. Contributed by @ukheni50 (#2745; fixes #2742).
- ship design-checklist path points at the installed gstack/review copy. Contributed by @Lockyer228 (#2717; fixes #2694).
- Feature markers live in GStack state, not project checkouts — the wave re-applied the CI seeding to the composite action and added an upgrade migration so nobody gets re-prompted. Contributed by @simonaltit (#2748; fixes #2728).
- memory-ingest: transcript pages no longer vanish to slug collisions (887 staged → 0 ingested) or a frontmatter-fence render bug; slug assignments persist in the ingest state, so a page keeps its slug across runs and a new collider can never overwrite an unchanged page. Contributed by @rayers (#2699; fixes #2724).
- browse js/eval returns the value of an async IIFE. Contributed by @loulanyue (#2747; fixes #2727).
- land-and-deploy checks fork branches in the head repository — the wave replaced the metadata command (gh leaves `nameWithOwner` empty) and made fork branches report-only. Contributed by @pttydou (#2725; fixes #2696).
- gstack-config rejects malformed `cross_project_learnings` at set. Contributed by @szsunyuan (#2676; fixes #2673).
- gbrain-sync's pinned-source test is hermetic under a live autopilot. Contributed by @szsunyuan (#2689; fixes #2685).
- redact: stale managed pre-push hooks refresh in place, so shipped wrapper fixes reach existing installs. Contributed by @schienbiz (#2731).
- redact: Groq, Tavily, and Notion API key patterns. Contributed by @schienbiz (#2730).
- redact: `.env.local` is not an internal hostname. Contributed by @davidani-davi (#2740).
- redact: git SSH remotes are not emails — the wave named the lookahead constant. Contributed by @alopes50 (#2734).
- gbrain ≥0.43 held-lock refusal classifies as engine-locked, not broken-config. Contributed by @pvanl (#2698).
- A slow gitleaks probe no longer permanently disables secret scanning — and after three consecutive slow answers the run stops re-probing per file (a loaded box no longer stalls a big ingest for hours), warning once; the next run probes fresh. Contributed by @deniszjukow (#2715).
- open-gstack-browser pre-flight actually kills the stale daemon (the pid grep never matched pretty-printed JSON). Contributed by @deniszjukow (#2714).
- browse CLI honours CHROMIUM_PROFILE in profile-lock cleanup — the wave added the wiring tripwire. Contributed by @adam-badar (#2732).
- make-pdf's pdftotext probe reads poppler's stderr banner instead of reporting "unknown" everywhere. Contributed by @snymanpaul (#2690).
- bin writers no longer drop learnings and question events on paths with apostrophes; MSYS-form GSTACK_HOME works — the wave unified all four writers on the env-var import pattern. Contributed by @shreshth-designs (#2720).
#### For contributors
- Tests: 8,519 → 8,660 (+141 across the wave; every behavior fix carries a regression test proven red on a v1.77.0.0 scratch worktree via base-compatible reproducers — tests that merely fail to compile on the base don't count as receipts).
- New structural guards: generated-doc fence pairing (CommonMark state machine, not mod-2 counting), no bare `${PIPESTATUS[0]}` in codex sections (plus real bash+zsh execution), OSV config wiring (flag ↔ filename ↔ entry hygiene, `ignoreUntil` mandatory), gstack-redact CLI empty-path rejection, cli.ts profile-dir delegation.
- 19 carve skeleton ceilings re-pinned with measured values (+~440 bytes/skill for the AUQ fence); autoplan re-pinned again for the broken-install preflight arm.
- `gen-skill-docs --link-root` exists for render-into-tmp-then-swap callers; direct-render callers need no change.
- The absorbed test files gained spawn timeouts (v1.77 sync-spawn tripwire predates them).
## [1.77.0.0] - 2026-08-31
**Every PR stops paying for evals twice.**
**Flakes are now measured, killed at the root, and fenced.**
The test infrastructure got its overhaul, wave 1. The legacy 17-row eval matrix that ran serialized AHEAD of the sliced lane on every PR is deleted: one paid lane, its gate census derived from the runner itself, so a new gate test is in the census the moment its file lands. No hand-enumerated rows to drift, and the drift already tried, a new matrix row landed on main mid-branch and the merge resolved to the derived census that covers it by construction.
The flake war moved from anecdotes to instruments. Every retried pass is now recorded where it cannot hide (bun's own output shows a retry as a clean pass, we probed it), the free lane retries a failing file once, loudly, and appends every flaky pass to a per-project ledger uploaded from CI on green runs. `bun run eval:flake-rank` ranks the series. And the wedge class that hit main, a hung child under a blocking spawnSync that no in-process timeout can interrupt, is extinct: 499 timeout-less sync-spawn sites across 146 files (the branch tripwire's own count against main) swept to zero, with a ratcheted tripwire that failed its first real offender the day a timeout-less spawn arrived from a merge.
### The numbers that matter
Source: CI runs 33263204465 / 33262077256 (measured 2026-08-29), the repo census (`test/helpers/touchfiles-data.ts`), and the sweep tripwire (`test/spawnsync-timeout-tripwire.test.ts`).
| Metric | Before | After | Δ |
|---|---|---|---|
| Paid lanes per PR | 2 (serialized) | 1 | eval wall 35.5 → ~13 min (target) |
| Measured duplicate spend per PR | ~$20.94 | $0 | matrix deleted |
| Gate census keys | 86 (6 phantoms) | 77, all provably alive | reverse invariant enforces |
| Sync spawns that can wedge a shard | 499 unbounded | 0 (8 reasoned exemptions) | tripwire-ratcheted |
| Local gate worst case | ~6.5 h (4×4 jobs) | ~3.3 h (8×2) | isolation landed first |
| Retried-pass visibility | invisible | recorded + ranked | `eval:flake-rank` |
The phantom-census number is the quiet one that matters: six merge-blocking "tests" existed only as map keys. They are deleted, and a key without a living test now fails the free suite.
What this means for anyone shipping here: PRs get one honest paid verdict faster and cheaper, a flaky pass never blocks your merge but never disappears either, and a test that hangs takes down thirty seconds, not a shard. Run `bun run eval:flake-rank` when you want the flake ledger's verdict.
### Itemized changes
#### Added
- **Flake telemetry, end to end.** Eval-store records every retry attempt (`attempt`, `flaky_retries`), the paid report lists passed-only-on-retry tests as warnings, the free lane's flaky-retry pass is ON in CI with a single-writer JSONL ledger (branch + sha attributed, per-project by default) uploaded as an artifact on every run, and `bun run eval:flake-rank` aggregates the series with final-attempt accounting and a 60-day recency bound.
- **Two-phase session timeouts.** A silent API dies at a startup grace (90s local, 300s CI floor, a real `Math.max` floor) with the distinct reason `timeout_startup` instead of burning a 600s work budget into an opaque `0 turns / $0.00` failure; the work budget arms on the first byte and the total wall never grows.
- **Green-by-skip census.** "Ran N tests" counts skips, so a codex/gemini file whose every test self-skipped used to read as coverage. The classifier now parses bun's skip/pass recap and the paid report labels an all-skipped pass "verified nothing."
- **Sync-spawn timeout tripwire** (spawnSync / execSync / execFileSync / Bun.spawnSync, comment-aware, 30-line window, shrink-only exemption ratchet) plus a raw-SHA fixture ban (`git show :path` fixtures must be vendored; the one live offender now reads a committed fixture).
- **Behavioral kill-semantics tests**: a fake-claude shim proves a timed-out session leaves neither the CLI nor its grandchild alive; two gstack-detach watchdog tests cover TERM-immune grandchildren and the leader-dies-first case.
- **CLI version stamping**: every eval run records `claude --version` (resolved once in the runner parent), so the next TUI-drift flake hunt is a grep, not archaeology.
#### Changed
- **One paid lane.** The legacy evals.yml matrix is deleted (pure deletion, one revert restores it) after a static parity receipt: the sliced lane's 49-file derived census strictly contained the matrix's 18 files. The PR comment moved into the sliced lane with final-attempt accounting and a fail-closed reconciliation verdict.
- **Paid runner defaults 4×4 → 8×2**: ~10-13 real in-flight sessions (under the documented-safe 15) instead of ~4-6; per-shard TMPDIR/Chromium-profile isolation and a kill-path cleanup backstop landed first, deliberately.
- **CI setup deduplicated into four composite actions**; the register-skills composite carries the fail-fast dangling-symlink verification loop that only the deleted matrix copy had, so the surviving lanes inherit it. Rerun-safe, frozen-lockfile fallback, input-validated.
- **Supply chain pinned**: the claude CLI in the CI image is an exact version (bumps ride PRs that run the PTY gate, ending the weekly-latest drift that broke the harness three times), and every action in the secrets-bearing and image-publishing workflows is SHA-pinned.
- **Routing journeys lost their answer key**: the fixture no longer ships a prompt→skill lookup table, so a regressed skill description can actually fail the test again, at roughly half the previous per-journey cost (2 turns, [Skill, Read] only).
- Decided A/B experiments retired (auq-repetition-cut, preamble-script, the opus-47 single-run fanout comparison): one-shot questions answered months ago no longer re-run weekly as coin flips. `plan-ceo-review-expansion-energy` and `ios-qa-e2e` moved to the periodic tier with reasons.
#### Fixed
- **A fail-open reconcile gate**: GitHub's default run-step shell has no pipefail, so the fail-closed report's exit was read from `tee` (always 0) in both paid lanes. Now `PIPESTATUS[0]`, pinned by a wiring test.
- **A write-token trust boundary**: the job that executes PR-authored code no longer holds the PR-comment write token; commenting moved to a job that runs zero repo code.
- **Provider-runner orphans**: timeouts kill the whole process group (claude, codex, gemini, and gstack-detach's watchdog with the group id captured at spawn), and the codex/gemini runners inherited the orphan-drain hardening only the claude copy had. The observed 600s-timeout-stretching-past-1400s class is gone, with a regression net.
- **Selection integrity**: 17 phantom selection keys deleted (a reverse invariant now requires every key to name a living test), gitignored `.agents/**` dep patterns that could never match a git diff replaced with their generators, and the codex/gemini local touchfile forks now derive from the canonical map.
- **A cross-shard SKILL.md race**: the opus-47 eval regenerated the live tree's skill files mid-run; it now renders into a scratch dir via `--out-dir`.
#### For contributors
- `bun run eval:flake-rank` (with `--json`, `--dir`, `--since-days`) is the promotion-clock dial; the WS16 required-check decision reads it.
- The overhaul plan (16 workstreams, reviewed by CEO + eng passes with two cross-model outside voices) continues: budget-aware shard walls, PTY readiness events, free-suite splits, judge determinism, and required-check promotion are the next waves.
## [1.76.0.0] - 2026-08-31
**Ship's doc-sync now survives Conductor.**
**Spawned subagents finally know they're spawned.**
Every Conductor-hosted /ship used to lose its PR `## Documentation` section the moment the document-release subagent hit an interactive gate: the subagent inherited the parent's environment, classified itself as a session a human was watching, rendered a decision brief nobody could answer, and stopped. The JSON contract broke, every time a gate fired (#2733). This release makes the spawned classification reachable: /ship marks its subagent with `GSTACK_SESSION_KIND=spawned`, and the whole stack (preamble, AskUserQuestion rules, both AUQ hooks) now resolves gates by auto-choosing the recommended option instead of writing prose to nobody. Destructive options are never auto-chosen, on any surface: the conservative choice wins and gets recorded. Auto-chosen decisions come back in a `decisions` array the parent prints to your console, so nothing is decided invisibly.
### The numbers that matter
Source: the new gate-tier E2E (`test/skill-e2e-docsync-spawned.test.ts`) run receipt under `~/.gstack/projects//evals/`, plus issue #2733's field reports.
| Metric | Before | After | Δ |
|---|---|---|---|
| `## Documentation` on Conductor-hosted ships | dropped whenever a doc gate fired | present, E2E-proven | fixed |
| Ways to reach the spawned classification | 1 (OpenClaw env only) | any dispatching skill, one env prefix | new primitive |
| Onboarding prompts consumable by unwatched subagents | up to 11 blocks per run, markers eaten | 0 emitted, markers preserved for the next human session | sealed |
| Real-agent E2E through a firing VERSION gate | prose-STOP, JSON parse fails | auto-chose the recommended Skip, JSON parsed, VERSION untouched | 1/1 pass, $0.35, 106s |
| Preamble dead work per spawned start | network update-check + a 17-subprocess repo probe | both skipped | faster subagents |
The E2E is the receipt that matters: a live agent ran the real document-release preamble and VERSION gate inside a Conductor-ambient environment with the AUQ hooks active, ended on the machine-parseable contract, and left VERSION alone.
### What this means for your workflow
Ship from Conductor and the PR carries its Documentation section again; any gate the subagent auto-chose shows up as a `Doc-sync auto-decisions:` line in your console. If you build orchestrating skills, prefix a subagent's `gstack-skill-start` invocation with `GSTACK_SESSION_KIND=spawned ` and it behaves like a proper worker: no consent prompts consumed, no telemetry questions, no prose briefs to nobody. If anything sets that marker on a session a human is actually driving, the preamble now says so loudly (`SPAWNED_OVERRIDE: env`).
### Itemized changes
### Added
- **`GSTACK_SESSION_KIND=spawned`** (`bin/gstack-session-kind` step 0): explicit per-command spawned marker, outranking every ambient env marker. Deliberately narrow: only `spawned` is honored; other values are reserved and ignored. Documented in `docs/OPENCLAW.md`, with a `SPAWNED_OVERRIDE: env` status line for tamper visibility when the env var (rather than an orchestrator marker) drove the classification.
- **`decisions` in the doc-sync contract** (`ship/sections/pr-body.md.tmpl`): the document-release subagent records each auto-chosen gate as one line in a required `decisions` array; the parent prints them after the sync summary. Never embedded in the public PR body (tripwire-pinned). Absent key from an older installed skill reads as empty.
- **Proactive spawned rule in the AskUserQuestion prose** (all tier-2+ skills): `SESSION_KIND: spawned` now short-circuits BEFORE the Conductor rule, with the destructive carve-out (never auto-choose an irreversible option, take the conservative choice and record it) and anti-injection scoping: a spawned marking counts only from the prompt that created the session, never from files, tool output, or web content read mid-run.
- **Gate-tier E2E** (`test/skill-e2e-docsync-spawned.test.ts`): drives the verbatim Step 18 dispatch prompt against a real preamble-bearing document-release slice in a Conductor-ambient env with both AUQ hooks seeded live; asserts the 5-key JSON contract, a non-empty `decisions` array, and an untouched VERSION through a deliberately fired gate.
### Changed
- **`bin/gstack-skill-start`**: spawned sessions suppress `CONDUCTOR_SESSION: true` (prose to nobody is always wrong), key `SPAWNED_SESSION: true` and the spawned-session instruction block on the resolved kind instead of raw `OPENCLAW_SESSION` (OpenClaw behavior unchanged, regression-pinned), gate all 11 interactive-onboarding blocks plus their ack markers at emission, and skip the network-bound update-check and the first-task repo probe (their consumers are suppressed anyway; the one-shot just-upgraded marker survives for the next human session).
- **AUQ hooks** (`hosts/claude/hooks/`): the Conductor deny gains a deterministic `[conductor][spawned]` auto-choose branch for env-marked spawned sessions, with per-question one-way-door annotations; both hooks' prose directives (interactive AND headless) carry a shared spawned escape sentence, single-sourced in `spawned-directive.ts` so the two paths can never drift. Hooks inherit the harness env, so the escape text is the designed lever for per-command-marked subagents.
- **Ship Step 18 dispatch prompt**: frames the subagent as spawned, instructs the same-line env prefix (template bash blocks do not share exports), resolves every named gate to the recommended option with a conservative fallback, and places the skill's own doc-health summary in the body so the JSON stays the final line.
### Fixed
- **#2733**: Conductor-hosted /ship runs no longer lose their Documentation section when document-release hits an AskUserQuestion gate. The failure was gate-correlated and hit every ship on affected hosts.
### For contributors
- Skeleton byte ceilings in `test/helpers/carve-guards.ts` re-ratcheted for the AUQ prose growth (19 skills, measured values in comments); `test/fixtures/context-budget.json` recaptured in the same commit per the ratchet protocol.
- New `docsync-spawned` selector in touchfiles (deps name every behavior under test) and a matching gate row in `.github/workflows/evals.yml`; `bin/gstack-session-kind` joined the `conductor-prose` and `auto-decide-preserved` selectors (it previously appeared in no dep list).
- `test/gstack-session-kind.test.ts`, both hook suites, the resolver suite, and the dispatch tripwire gained ~25 cases, including a `spawnedByEnv()`-vs-script parity pin and a cross-surface destructive-policy drift guard.
## [1.75.0.0] - 2026-08-29
**Your review now hunts over-built code, not just broken code.**
**And every skill's advice starts with "reuse before you build."**
This release imports the best of ponytail, the code-minimalism ruleset, without importing its build-less posture. /review gains an eighth lens: a simplification specialist that flags unrequested structure (hand-rolled stdlib, one-implementation abstractions, dead flexibility, dependencies duplicating platform features) in a closed five-tag vocabulary. It is advisory only. It cannot dent your quality score, its fixes are never auto-applied, and on a lean diff it tells you something no reviewer ever says: "lean already — nothing to cut." Every tier-2+ skill also gains the reuse ladder (stop at the first rung that holds: this repo, stdlib, native platform, an installed dependency... then build the complete version of what remains) and a bounded closer, so completion reports stop touring every edit. Accepted shortcuts now leave a durable trail: a decision-ledger entry plus a `gstack-shortcut(dec-)` marker in code, harvested into a debt ledger by /retro. Agent hosts without a full install (Zed, Amp, Cursor side projects) get a 1.8KB rules digest to copy into their own rules file. And /autoplan now runs the Eng review last, always, so the required shipping gate reviews the final amended plan instead of a stale one.
### The numbers that matter
Source: this branch's own runs, recorded under `~/.gstack/projects//evals/` and the commit receipts.
| Metric | Before | After | Δ |
|---|---|---|---|
| /review lenses | 7 | 8 (simplification, advisory) | +1 |
| Simplification lens on this branch's own 5,190-line diff | n/a | `net: -37 lines possible` | dogfooded |
| AskUserQuestion preamble section, per skill | baseline | -236 B (~9.7 KB across 41 skills) | gated by a live A/B: post-cut 7/7 format elements, substance equal to pre-cut |
| Instruction-tier artifact for rules-reading hosts | none | 1,765 B committed digest (2,048 B budget) | new |
| Skills-earn-their-tokens benchmark | none | 3 tasks x 2 arms (with/without skill), judged on the diff left behind | new |
| /autoplan phase order | CEO, Design, Eng, DX | CEO, Design, DX, Eng always last | the gate sees the final plan |
The A/B receipt is the one to trust: the AskUserQuestion cut only landed because a two-arm eval on real SDK captures showed the shorter render lost nothing (the gate outranked the approval, by design).
### What this means for your workflow
Run /review on a branch you suspect is over-built and read the `[ADVISORY]` rows plus the `net: -N lines possible` footer; nothing blocks, nothing auto-applies, you decide. Take a shortcut in an AskUserQuestion and it stops rotting silently: /retro reads the ledger back to you with its upgrade trigger. If you work in a rules-reading editor without a gstack install, copy `agents-digest/gstack-AGENTS.md` into your rules file and the ethos rides along.
### Itemized changes
### Added
- **Simplification review specialist** (`review/specialists/simplification.md`): closed tag vocabulary (`delete:` / `stdlib:` / `native:` / `speculative:` / `shrink:`), dispatched on diffs over 100 lines, `--simplification` force flag. Advisory carve-out end to end: excluded from the quality score and findings-count header, ASK-only in Fix-First, `[ADVISORY]` labels, parent-printed `net: -N lines possible` footer, and a `Simplification: lean already — nothing to cut.` zero-findings line. Precision guarded by a false-flag fixture (a complete-but-lean diff must yield NO FINDINGS) and coverage-vs-structure boundary text (tests, error paths, and edge cases are never deletion targets).
- **Reuse ladder in Search Before Building** (every tier-2+ skill): before writing new code, stop at the first rung that holds — a helper already in the repo, the stdlib, a native platform feature, an already-installed dependency — then build the complete version of what remains. Root-cause rule included: one guard in the shared function beats a guard in every caller.
- **Bounded closer** (every tier-2+ skill): after completing work, report what changed, what was skipped, what to watch — a few short lines, with report-shaped skills (/qa-only, /plan-*-review, /retro, /document-generate) explicitly exempt because their report IS the work.
- **Shortcut debt ledger**: accepting a Completeness ≤ 7 option on a durable-scope call now logs the ceiling and upgrade trigger to the decision ledger and marks each cut corner with `gstack-shortcut(dec-): , upgrade when `; /retro Step 11.5 harvests the markers, joins them on decision ids, and tags `unlinked` and `no-trigger` rot risks. Markers survive the redaction engine (pinned by test).
- **Instruction-only host tier**: `agents-digest/gstack-AGENTS.md`, a committed, generated, budget-capped (2,048 B) digest of the ethos, reuse ladder, and voice rules. Setup's openclaw/hermes explainer arms print its path for copy-in; setup never writes a user's AGENTS.md (tripwire-tested, including laundered write shapes). README's host table now matches what setup actually does.
- **With/without-skill arm benchmark** (periodic eval): three build-shaped tasks (a native-platform overbuild trap, a CRUD endpoint, a bugfix with planted decoys) run through real `claude -p` sessions twice — with the behavioral-layer skill installed and without — and the staged diff each arm leaves is judged for over-engineering on a 0-3 rubric with a full failure taxonomy (judge_error cells excluded from aggregates but surfaced; zero-diff arms are valid cells). Each cell also runs the fixture's own functional oracle (`checks=pass|fail|none`), so a refusal, a broken implementation, and working code stay distinguishable — correctness before LOC. A research instrument, not a gate.
- **/autoplan runs Eng last, always**: mandatory order is now CEO → Design (if UI scope) → DX (if developer-facing scope) → Eng, with a single final approval gate; clearly-wrong premises queue as User-Challenge items instead of stopping mid-run, and accepting one at the gate amends the plan and re-runs Eng on the amended plan. Static test pins the Eng-terminal order.
### Changed
- **AskUserQuestion preamble section slimmed** (-236 B per skill, ~9.7 KB across the corpus): duplicate statements of the completeness rule, auto-decide marker, and tool-not-prose rule removed while keeping every verbosity floor and all 14 format pins. Landed only after a live NOT-WORSE A/B (pre-cut vs post-cut render, identical prompt, SDK capture) showed zero format-element loss and equal recommendation substance.
- **Terse-mode label tells the truth**: the claimed savings is now the measured 2.6 KB, not "~3-5 KB".
- `/review` checklist knows Completeness Gaps and Simplification are orthogonal (coverage up, structure down), and a `gstack-shortcut` marker downgrades a would-be gap finding to acknowledged debt — but only when its decision id resolves in the ledger; an orphan marker is reported as a forged suppression (cross-model adversarial catch).
### Fixed
- **Version bumps no longer strand the agents digest**: `gstack-version-bump write --regen-digest` reruns the repo's digest generator in the same mutation (explicit opt-in — a plain `write` never executes repo files it merely finds), and both ship's and land-and-deploy's evidence gates allow-list the digest alongside VERSION. Without this, every release commit of this repo would have failed the freshness CI check.
- **The free-suite flaky retry can no longer mask real failures**: the retry pass vetoes on ANY unattributable failure evidence (headerless failures, unhandled errors between tests, truncated runs), an empty shard carries an empty failing-files list instead of crashing the retry, and a dead conditional was removed.
- **Version allocation distrusts laundered git**: `gstack-next-version` consults `ls-remote` only when origin is actually configured, and a configured origin that "successfully" advertises zero heads (the Conductor git shim's failure shape) now falls back to local refs with a loud warning instead of reading an empty queue and reallocating a taken version. Both shim shapes are pinned by regression tests.
- **Browse temp paths are portable**: local file serving accepts both the browse TEMP_DIR and the OS tmpdir (`TEMP_DIRS` allowlist), while remote serving stays pinned to TEMP_DIR only (the security asymmetry is test-enforced). An untrustable TMPDIR (`/`, `$HOME`, an ancestor of the daemon's cwd) is ignored rather than trusted for the daemon's lifetime.
- Setup's instruction-tier pointer prints a script-anchored digest path instead of a cwd-relative one that broke when invoked from another directory.
- /retro's shortcut harvest no longer reports phantom debt from files that merely document the marker convention (placeholder filter + judgment prose).
- The arm benchmark harvests against a recorded seed commit (immune to agents that commit and push), ignores node_modules, survives multi-megabyte patches, names its judge-diff cap and reports truncation loudly, and wraps untrusted diffs in per-call random sentinels so a faked closing marker cannot steer the judge.
- The AUQ A/B eval treats a judge failure on either side as inconclusive instead of coercing it to a fake degradation-or-mask, and reads its pre-cut arm from a vendored fixture instead of a branch-local SHA that dies with the branch.
### For contributors
- `bun run test` gains sandbox knobs: `GSTACK_FREE_JOBS` overrides shard count (digits-only, loud on garbage) and `GSTACK_FREE_RETRY_FLAKY=1` opts into one serial retry pass for syscall-supervised sandboxes (default stays OFF — dev boxes should see flakes). `scripts/sandbox-doctor.sh` makes a Vercel/Conductor cloud sandbox run the suite green in one idempotent command (documented in docs/TESTING_INTERNALS.md): atomic git-shim patching with a backup, loud on patch-pattern drift, :99-socket Xvfb detection, dnf-gated installs, and it survives a missing /dev/shm. A failed digest regen now fails `bun run gen:skill-docs` locally instead of deferring the red to CI.
- The arm-benchmark selftest (fixture integrity, judge plumbing, install asymmetry) now runs FREE in `bun run test` on every PR via `test/arm-benchmark-selftest.test.ts` — the paid periodic instrument can no longer burn money on broken fixtures.
- Eval-store schema v2: harvest records carry `{insertions, deletions, net}`; `recordE2E` now populates `tokens_used`.
- Touchfiles dep lists closed gaps (fixtures, judge helper, harness, ship render) and the context-budget ratchet fixture was re-captured, locking the AskUserQuestion reduction so it cannot silently regress.
- New coverage: TEMP_DIRS widening + remote-serving asymmetry, shortcut-marker writer/harvester grammar joint, sandbox-doctor shell guards, GSTACK_FREE_JOBS parsing, laundered-ls-remote shims, digest freshness/budget/writer tripwires, and a real generator round-trip through the version bump.
## [1.74.0.0] - 2026-08-29
**Green now means green: every test runs somewhere, provably.**
**The suites got faster by deleting lies, not by skipping work.**
This release is a full audit and overhaul of gstack's own test and CI system. The audit found the safety net lying in specific ways: three CI eval jobs ran zero tests and passed on every PR, four paid test files could never execute in any lane, the required free-tests check silently skipped nine make-pdf gates on Linux for their entire life, and about 57 E2E files ran in no scheduled lane at all. All of it is fixed, and each fixed class now has a tripwire so it cannot quietly return.
Speed came from structure. The free suite packs shards by recorded per-file durations instead of file counts, and the serial tree-mutating shard is gone entirely: the generator gained a main() guard and renders every host into out-dirs, so the suite never writes the live tree. The paid lane re-platforms CI onto the same sharded runner you use locally, with one planner manifest, sliced executors, and a report that fails closed when a slice's artifact never lands.
### The numbers that matter
Sources: live CI run 33194732051 (pre-change shard timings), the committed durations seed (`bun run test:free --record-durations`, 496 files), and the planner's own output on this branch.
| Metric | Before | After | Δ |
|---|---|---|---|
| Free-suite shard spread | 28s to 97s | 6 shards, ~80s predicted each | balanced |
| Serial mutator tail, every run | ~35-40s | 0s (shard dissolved) | gone |
| Paid files runnable in NO lane | 4 | 0 | tripwired |
| E2E files in no weekly CI lane | ~57 | 0 (3 reasoned excludes) | contract |
| Zero-test green CI jobs per PR | 3 | 0 | deleted |
| Touchfiles keys missing self-registration | 129 | 0 | enforced |
| Hand-tuned paid timeout literals | 395 | 97 (46 justified) | 5 tiers |
The self-registration number is the quiet one that matters most: before it, editing only a test's assertions selected nothing, so the changed test never ran on the change that changed it.
### What this means for you
`bun run test` is honest and flat: no hidden slop scan, no serial tail, shards that finish together. Paid CI and local paid runs share one engine, so a shard that never starts, a slice that dies, or a file that self-skips everything is a red check with a name, never a silent pass. When you add a paid test, the orphan tripwire forces it into the census the same commit. Upgrade, run `bun run test:free`, and read `docs/TESTING_INTERNALS.md` if you maintain tests.
### Itemized changes
#### Fixed (what green means)
- The required free-tests lane builds the gate binaries (`build:gates`) and runs the nine make-pdf e2e gates that silently self-skipped on Linux since they existed; `GSTACK_EXPECT_BINARIES=1` + `ci-prereqs.test.ts` invert the skip polarity in CI so the class cannot return.
- Deleted the two vestigial eval matrix rows that ran zero tests per PR (codex/gemini, periodic-tier files with no row tier) and armed `e2e-pty-plan-smoke` with its missing `tier: gate` (it burned ~7 minutes of setup then skipped every describe).
- Activated the four paid test files whose names fell outside the paid globs (net execution zero, forever): carve-section-loading, codex-e2e-plan-format (+ its missing periodic gate), codex-e2e-recommendation-substance, llm-judge-recommendation. New `paid-orphan-tripwire.test.ts` fails the suite on any EVALS-gated file outside the globs.
- 135 touchfiles keys now name their own declaring test file; the tier-alignment warning became a hard failure with a 4-entry ratchet.
- Five quarantined browse tests reactivated (two guard the extension's privileged-message security boundary); root cause was stale dev-machine state, proven byte-identical since v1.66.
- The two `expect(true)` paid stubs are `test.todo` (reported as todo, never pass), keeping their selector surfaces.
- Five test files stopped assigning `GSTACK_HOME` at module scope (it leaked into every sibling in the shard process); a static tripwire blocks recurrence.
- Shared `/tmp` artifact paths in six PTY tests became per-test mkdtemps (they collided under retry and parallel worktrees); 18 live-repo `cwd:` sites audited and reason-commented.
- `restrictDirectoryPermissions` warns and skips symlinked dirs on both platforms (chmod and icacls dereference the link), closing the Windows lane's standing red with a platform-aware regression test.
- Judges resolve their model through `lib/eval-model.ts` (the global `GSTACK_EVAL_MODEL` override now applies) and retry 429s with jittered exponential backoff instead of one fixed second.
- Seven 28-minute test timeouts inside 25-minute CI jobs trimmed to the physical ceiling; an `eval-budgets` fit test pins that budgets above the wall cannot come back.
#### Changed (speed and structure)
- Free suite: duration-aware LPT shard packing from the committed seed (`scripts/free-test-durations.json`, refresh with `bun run test:free --record-durations`), duration-aware wall timeouts, per-shard prediction logging, corrupt-seed fallback to hash sharding. The `--shard` CI-matrix contract is untouched.
- `TREE_MUTATING` is empty: `gen-skill-docs.ts` gained a `main()` guard (imports never regenerate; pinned by an import-purity test) and `--out-dir` renders every host, so all eight former mutators render into mkdtemps and the four ratchet readers rejoined the parallel shards.
- Paid runner: full-stream spooling to per-shard log files (no more 30-minute streams held in RAM), shared `runShardChild` lifecycle with the expectedFiles enforcement drift fixed, parent-computed selection propagated to children via `EVALS_SELECTION_JSON` (fail-open), retry parity as literals.
- Paid CI re-platform (parity phase): evals.yml gains a sliced lane (planner manifest, 6 executors, fail-closed report) running alongside the legacy matrix; evals-periodic.yml runs ALL periodic-tier tests weekly minus the reasoned exclusions in `periodic-exclude-data.ts`, plus a weekly full-gate census and a tracking-issue upsert on red weeks. The hollow-shard guard fails exit-0 shards that executed zero tests under `EVALS_ALL`.
- 298 paid timeout literals swept onto five named tiers (`test/helpers/eval-budgets.ts`), round-up only.
- `slop:diff` left `bun run test` (it silently added up to 240s) and runs in quality-gate per PR instead; `/review` keeps its interactive run.
- The four worst fixed sleeps (300s/30s/30s/20s) became condition polls or stdin-EOF-bound child lifetimes; the parent-watchdog test dropped from 24s to 3.6s with a strictly stronger assertion.
- CI hygiene: least-privilege permissions on every workflow, one pinned Bun version everywhere (drift-tested), the image-tag triple bound by test, ci-image stops rebuilding identical images on every ship, quality-gate dropped its 74-second full-history checkout, fork-safe concurrency keys, timeouts on every job, windows caches warm-start on lockfile bumps.
#### Added
- 95 tests for six zero-coverage surfaces: the eval CLI family (eval-list/compare/summary/select), slop-diff, the code-intelligence CLI, browse media-extract and session-cookie-store, and lib/version-source.
- Policy and tripwire tests: paid-orphan tripwire, GSTACK_HOME module-scope tripwire, bun-version drift, image-tag binding, gen-skill-docs import purity, out-dir byte-identity for external hosts, manifest/slice/report contract, eval-budget fit and ratchet, periodic-exclude policy, selection propagation drift.
- `test/helpers/run-bin.ts`: one spawnSync wrapper replacing ~36 near-identical local `run()` helpers (first three files migrated; the rest are a filed follow-up).
#### For contributors
- `docs/TESTING_INTERNALS.md` documents the new runner architecture; CLAUDE.md's testing prose matches it. TODOS.md closes the absorbed backlog items (periodic coverage contract, eval-harness observability, the sidebar trio, which turned out already deleted) and files the follow-ups: legacy matrix deletion after parity, the required-check decision, browse /tmp-namespace hardening, PTY boot-readiness waits, the single typed test registry, and the bun-native LPT swap at the next Bun unpin.
## [1.72.0.0] - 2026-08-28
**"Go register an API key" now drives your real browser.**
**Aside is the recommended driver, consent asked every single time.**
When a workflow hits a third-party website moment, registering an API key, creating a vendor account, wiring a webhook, gstack now checks for the Aside AI browser and offers to drive it: your real logged-in sessions, no cookie export, no re-auth. You approve each drive per task, by name and by site. Passwords, payment, CAPTCHAs, and identity stay yours; Apple credential creation is never a drive target in any skill. No Aside installed? On a Mac you get one download pointer (aside.com, macOS 15+), once, and gstack's own visible browser remains the fallback everywhere. gstack never runs an installer for you, and a detected binary is never treated as consent.
This release also fixes a real hardening bug: on hosts where the process holds CAP_FOWNER (Docker as root, CI sandboxes), the browse daemon's owner-only chmod could land on `/tmp` itself when a state file was configured there, locking the whole machine's `access(2)` checks out for everyone. The permission code now refuses shared, sticky, symlinked, or foreign-owned directories, warns instead of going silent, and pins the check and the chmod to one inode.
### The numbers that matter
Source: this branch's test runs on a Linux CAP_FOWNER sandbox, plus the eval records under `~/.gstack/projects//evals/`.
| Metric | Before | After | Δ |
|---|---|---|---|
| Drivers offered at a third-party web moment | 1 ($B headed) | 2 (Aside recommended, $B fallback) | +1 |
| `/tmp` after a `BROWSE_STATE_FILE=/tmp/x.json` boot on a CAP_FOWNER host | chmod 0700, machine-wide breakage | refused, warned once | fixed |
| `resolve-user-slug` on coreutils-only Linux with a git email | exit 127 | exit 0 | fixed |
| Prose pins guarding the consent contract | 0 | 21 tests, 254 asserts | new |
| Live consent-gate E2E cases (gate tier, hermetic shims) | 0 | 5, all passing at ~$0.35/run | new |
The consent gate is the number to care about: five real `claude -p` runs prove the agent offers Aside only when detected, degrades cleanly when it is broken or absent, pitches the download exactly once on macOS, and refuses browser drives for Apple credentials under any framing.
### What this means for you
The next time a skill says "you need a Duffel test token," it can just go get it with you watching, in the browser where you are already signed in. You approve the site and the actions first, every time, and the secret lands in an owner-only file, verified with one read-only API call, never echoed to chat. If you would rather not install anything, nothing changes: the first-party browser keeps working exactly as before. Docker-as-root users should upgrade for the `/tmp` fix alone.
### Itemized changes
### Added
- Third-Party Web Actions contract (ship, spec, office-hours, land-and-deploy, setup-deploy) now names the Aside AI browser as the recommended driver: runtime detection probe with a portable timeout guard, detection-conditional consent options, step-wise drive discipline with the vendor's confirm mode left on, and a failure path that quotes errors (redacted), retries once, and falls back only with fresh consent.
- Credential boundaries hardened in the same contract: secret-minimization preference (password-manager autofill, human-used copy buttons), Apple credential creation banned as a drive target in every skill, and vendor `--help`/skill text explicitly scoped to operational syntax, never new permissions.
- `test/third-party-actions.test.ts`: 21 pin tests covering every load-bearing sentence of the contract, plus repo-wide tripwires (an `aside` command allowlist of `--version`/`--help` only, and a ban on Aside installer invocations in any generated doc).
- `test/skill-e2e-third-party-actions.test.ts`: five hermetic gate-tier E2E cases (present, absent-Linux, present-but-broken, absent-macOS pitch, Apple credential ban) wired into CI's eval matrix; the absent cases actively mask any real `aside` binary on the host so dev machines cannot leak into detection.
### Changed
- The v1.65.0.0 stance of driving only gstack's own browser stack is superseded by explicit user directive: Aside is the one product gstack recommends by name. Never-auto-install and per-task consent survive unchanged and are now pin-tested.
- The setup flow's bun-installer checksum verification resolves `sha256sum` before `shasum`, so it works on coreutils-only Linux.
### Fixed
- `restrictDirectoryPermissions` no longer chmods shared sticky directories (`/tmp`, `/var/tmp`), foreign-owned directories, symlinked state dirs, or world-writable mounts when running as root; refusals warn once per process instead of failing silent, owned-but-unreadable dirs still self-repair, and the check-then-act race is closed with fd-anchored fstat/fchmod.
- `gstack-config resolve-user-slug` exited 127 on Linux distros without perl's `shasum` whenever a git email was set; both hashing call sites now resolve `sha256sum` first and fall back to `shasum -a 256`.
- A path-validation test assumed `/etc/crontab` exists (absent on Amazon Linux and minimal Fedora); it now uses `/etc/passwd`.
### For contributors
- `test/helpers/fs-caps.ts`: functional capability probes (`canRevokeWrites`, `canRevokeReads`) replace uid-0-only guards across 14 chmod-based test files, so suites skip honestly on CAP_DAC_OVERRIDE containers instead of asserting revocations the kernel ignores.
- Five `tpa-*` entries registered across `E2E_TOUCHFILES`/`E2E_TIERS` with template-level deps, the eval matrix row carries `tier: gate`, and `eval:bg:periodic`'s detach timeout rose to 36000s to cover the grown periodic shard census (floor-enforced by test).
## [1.71.0.0] - 2026-08-27
**Every skill invocation just got half the prompt bill.**
**Same behavior, measured by A/B evals, locked by CI ceilings.**
Every gstack skill pays a fixed prompt cost before doing any work. This release cuts that cost across all 62 skills and pins the wins so they can't creep back. The shared preamble's bash moved into two runtime scripts (`bin/gstack-skill-start`, `bin/gstack-skill-end`) that echo the same STATUS lines the prose always interpreted. One-time onboarding text now appears only when its gate actually fires, emitted as session-bound instruction blocks instead of riding along in every render. Eleven more skills got the section carve and office-hours' existing carve went deeper, taking the carved roster from 9 to 20: heavy reference bodies load on demand at the step that needs them, never before.
### The numbers that matter
Source: `bin/gstack-context-bill --diff` comparing the main render against this branch's render, both regenerated from source.
| Ledger | Before | After | Δ |
|---|---|---|---|
| /review eager per invocation | 109.5KB (~26.6K tok) | 53.7KB (~13.0K tok) | −51% |
| /land-and-deploy eager | 109.8KB | 54.4KB | −50% |
| /codex eager | 100.0KB | 53.9KB | −46% |
| Corpus on disk | 6.4MB (~1,651K tok) | 5.4MB (~1,398K tok) | −15% |
| Repo CLAUDE.md (always-on in dev sessions) | 66.4KB | 44.9KB | −32% |
50 of the 62 installed skills dropped (the rest are fixture/alias entries with no preamble to shed). The smallest real cut is −4,780 tokens per invocation (tier-1 utilities); non-carved tier-2 skills each shed a flat ~20.8KB of preamble. Zero always-on or eager growth anywhere in the diff.
The behavioral proof ran before this shipped: an A/B eval pins the script render against the old inline render, a section-loading eval verifies a real agent Reads each carved section before doing its step (20 skills, data-driven), and the full paid gate passed with the environmental-only baseline. The context-budget ratchet re-captured after every wave, so each ceiling now sits at the new, lower number.
### What this means for you
The agent reads roughly half the boilerplate before starting your task, so first-token latency and per-invocation cost drop across every skill you run. Onboarding prompts you already answered never render again. Nothing else should feel different: if a skill behaves differently than it did on v1.69, that's a bug, and the A/B harness exists to catch it. Run `bun run test` after upgrading; the ratchet will tell you if anything grew.
### Itemized changes
### Added
- `bin/gstack-skill-start` / `bin/gstack-skill-end`: the preamble and telemetry runtime, replacing ~18KB of inline bash per tier-2+ skill. Emits a `SKILL_START_PROTO: 1` handshake, STATUS lines, and gated one-time onboarding as `GSTACK_INSTRUCTION` blocks bound to a per-run session ID with a random suffix; passthrough output is sanitized so repo or prior-session content can never mint directive blocks or forge the session ID.
- `bin/gstack-retro-metrics`: deterministic git metrics for /retro (labeled contract, local reads only), replacing inline git/awk in the skill body.
- Section carves for 11 new skills — review, codex, land-and-deploy, autoplan, spec, setup-gbrain, qa, browse, retro, design-html, design-shotgun — plus a deeper office-hours carve (Phase 2A/2B), each with registered guards, loading scenarios, and recomputed size floors. The design carves force-read their UX doctrine before design work begins.
- Context-budget ratchet: a free CI test grades the always-on catalog and each skill's per-invocation cost against committed ceilings; growth fails the suite, reductions re-capture and lock.
- Six reference docs extracted verbatim from the repo CLAUDE.md (browser internals, CHANGELOG format spec, project tree, hermetic-E2E notes, slop-scan guide, OpenClaw publishing), each replaced inline by a short rule plus pointer.
### Changed
- The AskUserQuestion tool-resolution and 5+-option rules render as a compact branch table keyed on echoed STATUS lines; full split/CJK rules live at absolute install paths read on demand. All 14 mandatory format pins stay in every tier-2+ skeleton.
- ios-fix, ios-clean, ios-sync, and ios-design-review dropped to preamble tier 2 (they never used the tier-3 sections).
- The preamble degrades safely on stale installs: missing handshake means safe defaults, deferred onboarding (consent is never lost), and a one-line upgrade hint.
- The privacy consent gate and telemetry prompt fire only in interactive sessions; spawned and headless runs defer them to the next human session.
- Hermetic E2E children get seeded onboarding state in their own `GSTACK_HOME`, so evals never burn turns on first-run prompts.
### Fixed
- The once-daily artifacts pull is now non-interactive and slow-network bounded, so a hung remote can't stall the first skill invocation of the day.
- Branch names and artifacts-repo state files are sanitized before entering STATUS output and timeline records, closing log-forgery paths via hostile ref names or planted files.
- Skill-start no longer parses a multi-megabyte `~/.claude.json` on every invocation when no gbrain server is registered.
### For contributors
- Preamble A/B eval (`skill-e2e-preamble-script-ab`, periodic tier) pins script-render behavior against the pre-consolidation inline render; the carve-section-loading eval covers all 20 carved skills at an honest 480s ceiling.
- New free tests: skill-start/skill-end contract and behavior (13), retro-metrics (11), onboarding moved-literals tombstone (3 tests pinning 12 literals both directions), context-budget ratchet (7). Parity baseline and ratchet fixtures re-captured; the shrink floor stays (OV8 evaluated).
- `test/helpers/touchfiles-data.ts`: the runtime scripts joined every dep list that named the moved generators, so diff-based eval selection still fires on script changes.
## [1.70.1.0] - 2026-08-26
**Ship names its documentation subagent at every decision point.**
**The handoff is now pinned by tests that fail loud if it ever goes quiet.**
`/ship` has dispatched `/document-release` as Step 18 since v0.18.2.0, but the v1.54.0.0 carve moved that step into an on-demand section and the always-loaded skeleton stopped saying "document-release" at any decision point (one mention survived, buried in the re-run checklist). The wiring was intact. The visibility was gone, and nothing tested the handoff. This release restores the visibility and locks it in: the section index, the STOP pointer, the Step 17 handoff line, and a new hoisted doc-sync invariant all name "the /document-release subagent" (subagent-framed on purpose, so an agent dispatches the isolated worker instead of running a weaker inline copy). A free tripwire pins the wording, carve-guard anchors pin each touchpoint independently, and a new gate-tier E2E proves a live agent actually fires the dispatch before creating the PR.
### The numbers that matter
Source: this branch's eval store (`~/.gstack/projects//evals/`, runs of `test/skill-e2e-ship-docsync.test.ts`) and `wc -c ship/SKILL.md`.
| Property | Before | After |
|--------|--------|-------|
| Doc-sync subagent named in the Claude-host ship workflow body | 0 mentions at any decision point | 4 (section index, STOP pointer, Step 17 handoff, hoisted invariant) |
| Tests pinning the ship→document-release handoff | none | 5 free tripwire tests + 3 per-touchpoint carve anchors + 1 gate E2E |
| Live dispatch proof | never measured | 9/9 runs fire the dispatch before PR creation ($0.63-1.04, 234-319s each, sonnet-4-6) |
| Always-loaded skeleton cost | 91,267 B | 91,764 B (+497 B, cap raised to 92,300) |
Nine out of nine live runs is the line that matters. The E2E asserts on the actual tool-call stream, with a dispatch-specific matcher that a subagent merely quoting section text cannot satisfy, and a timeout-tolerant exit check that never softens the dispatch assert itself.
### What this means for gstack users
When you run `/ship`, the docs sync step is no longer an invisible line in a file the agent may summarize past. It is named at the exact moments the agent decides what to do next, and a merge-blocking test fails if any future edit makes it invisible again. A failed docs subagent still never blocks your ship. Nothing to configure. Upgrade and ship.
### Itemized changes
#### Fixed
- `/ship`'s Claude-host skeleton names "the /document-release subagent" at all three Step 18 decision points (manifest trigger rendering into the section index and STOP pointer, the Step 17 handoff line, and a hoisted doc-sync invariant beside the PR-title invariant). The invariant states the contract plainly: the dispatch itself is never skipped; only a failed subagent is non-blocking.
#### Added
- `test/ship-document-release-dispatch.test.ts`: free tripwire pinning the carved Step 18 contract (imperative, `subagent_type`, JSON return keys, non-blocking clause), the three skeleton touchpoints, the invariant-above-STOP ordering, the E2E matcher's four marker strings, and the inlined Step 18 → Step 19 ordering in the codex/factory goldens.
- `test/skill-e2e-ship-docsync.test.ts` (`ship-docsync`, gate tier): a live agent runs the sliced Step 17→19 ship tail in a hermetic git fixture; hard assert that an Agent dispatch matching the Step 18 prompt markers appears in the tool-call stream before any `gh pr create`. Fixture fails loud on step-marker drift, pins its git branch against operator config, asserts every setup command, and neutralizes the credential pre-push guard's question branch.
#### For contributors
- Carve-guards ship entry: three non-overlapping per-touchpoint anchors (gerund, imperative, third-person: no anchor subsumes another, so each is independently enforced), the carved imperative pinned to stay carved, skeleton byte cap 91,600 → 92,300 with the measured value recorded.
- `ship-docsync` registered in `E2E_TOUCHFILES` and `E2E_TIERS` (gate), with a whole-file `describeE2ETier('gate')` self-gate composed with diff selection so the file stays out of the periodic shard census; the tierless `test:evals` invisibility tradeoff is documented in the file header.
- Internal backlog notes corrected to describe the current Step 18 design (the pre-v1.54 "Step 8.5" prose was still documented as current), plus three deferred follow-ups recorded: a machine-checkable dispatch receipt, the same dispatch-pin treatment for land-and-deploy→canary, and the periodic shard-census ceiling arithmetic.
## [1.69.0.0] - 2026-08-22
**The silent-failure wave: tools that reported success while doing nothing —**
**or the wrong thing — now do what they say, or say loudly that they couldn't.**
Every fix in this wave closes the same failure shape. `gstack-evidence` — the tool other tools believe — certified runs whose environment differed from CI's, because bun auto-loaded the repo's `.env` files into every child it spawned. The gbrain wireup's first sync targeted the brain's *default* source, which could silently repoint a user's primary knowledge source at the gstack worktree while the just-registered source got zero pages — and still print a success line. `./setup --host slate` exited 0 having installed nothing. `land-and-deploy`'s merge recovery re-established everything except the `--delete-branch` half it had promised, and said nothing. A `_unattributed → deny` ingest policy never applied to exactly the pages it names. Skill-dir cleanup structurally could not find orphans. And a false-green test fixture meant the "gbrain missing" case could never fail on any machine with a real gbrain installed. Six fixes are new; five community PRs are absorbed with credit; ~17 tracker items close with receipts.
### The numbers that matter
Source: the regression tests named in each commit — every one verified to FAIL on a scratch worktree of v1.68.3.0 during the wave (the receipts standard), plus the live pre-fix probes quoted in the PR.
| Property | Before | After |
|--------|--------|-------|
| `gstack-evidence run` in a repo with `.env`/`.env.local` | child inherits bun-injected vars; ledger certifies a run CI never performs | value-equality scrub (shell-exported overrides survive), names-only warning, `GSTACK_EVIDENCE_KEEP_DOTENV=1` opt-out. Contributed by @namtrok (#2652) |
| Wireup first sync | `sync --repo` resolves against the DEFAULT source; can repoint its anchor, registered source gets 0 pages, prints success | `sync --source `; `--help`-probed with `--repo`+warning fallback for old gbrains |
| `./setup --host slate` | exit 0, installs nothing | explains Slate (use `--host claude`), exit 0 informational; any future accepted-but-unwired host exits 1 loudly; accept-list ⊆ dispatch-arms is test-pinned |
| land-and-deploy merge recovery | `--delete-branch` half silently dropped | `ls-remote` reconciliation: already-clean (idempotent) / confirm-first delete / "couldn't verify" — a failed check is never read as a clean branch |
| Orphaned skill dirs after the payload is gone | cleanup scanned the payload → structurally can't reap | destination scan, dangling-symlink aware, path-segment provenance. Contributed by @szsunyuan (#2634) |
| `gstack-redact install-prepush-hook` on bun/Windows | EEXIST crash — credential guard silently absent | `mkdirpSync` tolerates dir-EEXIST only. Contributed by @Lockyer228 (#2641); swept to decision-log, evidence, and the prepush skip-log |
| "gbrain missing" test on a box with real gbrain | saw the host's gbrain, exited 0 — could never fail where the bug exists | hermetic root-owned-dirs-only PATH + determinism check. Contributed by @SomSamantray (#2615) |
| Heredoc bodies ≥512B under Homebrew bash 5.2+ | child deadlocks (macOS 512-byte pipe buffer) | `BASH_COMPAT=50` guard in the 11 in-window scripts + a repo-wide scanner ratchet. Contributed by @BenjaminDSmithy (#2640) |
| `_unattributed → deny` policy under `--include-unattributed` | never applied — raw `""` remote bypassed the filter | stored remote matches the frontmatter sentinel; deny/read-only now bite |
| Brains on gbrain's ZeroEntropy recipe | embedding dies silently after Sept 4, 2026 | wireup warns on config detection (fail-open); setup-gbrain + docs advisories (#2365, gbrain-side migration stays open) |
| make-pdf sibling browse resolution | cwd-dependent (`dirname(argv[0])` is `.` in compiled binaries) | `process.execPath`-based; a decoy `browse/` directory can never win |
### What this means for you
Evidence verdicts are the run CI would perform — your shell-exported overrides still win, and the scrub tells you (key names only) what it removed. Revoking, cleaning up, and installing now either do the thing or name the thing they couldn't do. If your gbrain is on the dying ZeroEntropy recipe, gstack tells you before September 4 instead of letting search quietly rot. And five contributors' PRs are in this release with their authorship on the commits and their handles below.
### Itemized changes
#### Added
- Zero-dispatch guard in `setup`: a host that passes `--host` validation without an install arm errors loudly (names the host and the valid targets) instead of exiting 0 having configured nothing; cross-check test pins the accept-list against `hosts/index.ts` and every accept-listed host to a dispatch arm (#2361).
- ZeroEntropy sunset advisory: fail-open config detection in the wireup, provider-comment warnings in `/setup-gbrain`, and a troubleshooting entry in `USING_GBRAIN_WITH_GSTACK.md` (#2365 — refs; the gbrain-side migration remains open).
- `lib/fs-utils.ts` `mkdirpSync` (dir-confirmed EEXIST tolerance) with a bun-Windows-emulating preload fixture, applied to `gstack-redact`, `gstack-redact-prepush`, `gstack-decision-log`, and `gstack-evidence`. Contributed by @Lockyer228 (#2641; fixes #2635).
- Repo-wide heredoc scanner: any tracked shell script with an unguarded 512B–64KiB heredoc fails the free suite. Contributed by @BenjaminDSmithy (#2640).
#### Changed
- `land-and-deploy` §4a-postfail MERGED recovery reconciles the remote branch (three-way: already-clean / confirm-first delete / couldn't-verify) and states the outcome instead of staying silent (#2656).
- `make-pdf` resolves the sibling browse binary from `process.execPath` with an injectable test seam; the `about:blank` half of #2156 was already fixed in v1.64.0.0 (`browse/src/url-validation.ts` exact-match allow).
- `./setup --host slate` is an informational exit pointing at `--host claude` (per `docs/designs/SLATE_HOST.md`, Slate reads `.claude/skills` as a compatibility fallback) (#2361).
#### Fixed
- `gstack-evidence` scrubs bun-auto-loaded dotenv vars from the child env by value equality — a shell-exported override with a different value survives, `NODE_ENV=test` semantics mirror bun's, and an unreadable `.env` fails open (test-pinned). Known limitation documented in-code: bun-expanded `${VAR}` values are left in place (fails open). Contributed by @namtrok (#2652; fixes #2624; @harjothkhara's #2630 credited for the parallel diagnosis).
- Wireup first sync targets the registered source id, never the default source (#2662); support-probed with a warning fallback so gbrains at the 0.18.0 floor keep working.
- `cleanup_old_claude_symlinks` reaps orphans from the DESTINATION skills dir (dangling symlinks included) with path-segment provenance instead of a bare `*gstack*` substring. Contributed by @szsunyuan (#2634; fixes #2204).
- `gstack-memory-ingest` stores the normalized `_unattributed` remote so repo policies keyed to it actually apply under `--include-unattributed` (#2353).
- Hermetic gbrain-missing PATH fixture kills a false green on every machine with a real gbrain install. Contributed by @SomSamantray (#2615; fixes #2255).
#### For contributors
- Tests: 8,036 → 8,078 (+42 across the wave; every behavior fix carries a regression test proven red on v1.68.3.0).
- The heredoc scanner now gates every tracked shell script — new scripts with 512B–64KiB heredoc bodies need the `BASH_COMPAT=50` guard (or smaller/file-based bodies).
- On bash 4.3/4.4 (e.g. Git Bash), `BASH_COMPAT=50` prints a non-fatal `invalid value` stderr warning; those bashes never took the pipe path, so the guard is a no-op there.
## [1.68.3.0] - 2026-08-20
**Re-pairing a browser agent to narrow its access now revokes the old access on**
**the spot, revoke frees the agent's tabs, and `root` is a reserved client name.**
Tightening a paired agent is supposed to be one re-pair away. It wasn't. `POST /pair` minted a fresh setup key but never touched the agent's live session, so `pair-agent --client codex --restrict read` against an agent that had already connected (or whose new 5-minute key simply expired unexchanged) left the original full-access session, `eval` included, alive for up to 24 hours. Revocation also never released tab ownership, so an agent re-paired under the same name inherited the previous one's authenticated tabs. And because `root` is the sentinel the scope, domain, rate, and tab checks use for the omnipotent caller, `--client root` minted a "scoped" token that skipped all of them.
### The numbers that matter
Source: the before/after `BROWSE_HEADLESS_SKIP=1` daemon transcript in the PR and the regression tests in `browse/test/pair-agent-e2e.test.ts`, `browse/test/token-registry.test.ts`, and `browse/test/tab-isolation.test.ts`, which fail on the previous release.
| Property | Before | After |
|--------|--------|-------|
| Re-pair to narrow, agent hasn't reconnected | old wide session lives ~24h | old session 401s immediately |
| Narrowing re-pair before the agent connects | stale broad setup key still exchangeable | broad key dead, only the narrow key works |
| Broaden/refresh re-pair mid-task | old session lingers alongside a new key | working session kept, stale key dropped, no outage |
| Revoke a paired agent | tabs stay owned; same-name re-pair inherits them | tab ownership released; own-only access denied |
| `--client root` | "scoped" token bypasses all enforcement | rejected with a named 400 and a CLI fast-fail |
### What this means for you
Re-pair is now the real tightening lever. Re-pair an agent with its **same `--client` name** and a narrower `--restrict`/`--domain`, and the previous session is revoked and its tabs released the instant you run it, so the old access can't linger while you wait for the agent to reconnect. Broadening or refreshing the same agent leaves its working session alone, so you never strand an agent mid-task. Revoking (or a narrowing re-pair) also frees the tabs the agent opened, so reusing a client name can't hand the next agent someone else's logged-in page. `root` is rejected as a client name on both the CLI and the daemon.
### Itemized changes
#### Fixed
- A reducing re-pair (`/pair` with fewer scopes, tighter domains, a lower rate, or a stricter tab policy) revokes the client's live session and releases its tabs before minting the new key; the response carries `superseded`. Non-reducing re-pairs keep the session and only drop stale pending setup keys, so a broaden or refresh never strands a working agent. The requested grant is validated before any revoke, so a re-pair with a bad scope or rate is rejected without knocking the live session offline. (`browse/src/server.ts`, `browse/src/token-registry.ts`)
- A narrowing re-pair issued before the agent connects invalidates the earlier, broader setup key, so it can no longer be exchanged. (`browse/src/token-registry.ts`)
- Revoking an agent releases the tab ownership it held: `DELETE /token` runs the release unconditionally (ownership outlives the token) and reports `tabs_released`, and an own-only client re-paired under the same name can no longer read those tabs. A re-pair with no live session likewise frees any tabs orphaned by an expired incarnation, so a fresh session can't inherit them. (`browse/src/browser-manager.ts`, `browse/src/server.ts`)
- `root` is rejected as a `clientId` at every token writer, so a scoped token can never carry the sentinel that bypasses scope, domain, rate, and tab checks; `/pair` and `/token` return a named 400 and the CLI rejects `--client root` before it reaches the daemon. A persisted `root` entry is skipped when the registry is restored. (`browse/src/token-registry.ts`, `browse/src/cli.ts`)
#### For contributors
- Regression coverage pins each property: the reduce / broaden / shadow-key re-pair behaviors and the `grantReducesAccess` truth table (scope, domain direction, rate `0`=unlimited, tab policy) in `browse/test/pair-agent-e2e.test.ts` and `browse/test/token-registry.test.ts`; tab-ownership release and post-release denial in `browse/test/tab-isolation.test.ts`; reserved-name rejection across writers, routes, and registry restore.
## [1.68.2.0] - 2026-08-20
**Revoking a paired agent now revokes everything it holds, and the**
**documented kill switch is real: tunnel revoke deletes, then proves it.**
Revoking a remote agent was broken twice over. `revokeToken` deleted only the first token matching the agent's name, and the spent setup key kept for connection retries always sat first in line. So `DELETE /token/` returned 200 while the live session kept working, a leftover unspent setup key could mint a brand-new session for a "revoked" agent (inside the key's 5-minute validity), and a second DELETE returned 200 again. Meanwhile the documented way out, `$B tunnel revoke`, did not exist: the CLI forwarded it to the daemon as an unknown command. The pairing docs also promised a read+write sandbox three releases after pairing deliberately switched to full page access.
### The numbers that matter
Source: the before/after curl transcript in the PR (a `BROWSE_HEADLESS_SKIP=1` daemon on each branch) and the regression tests in `browse/test/token-registry.test.ts` and `browse/test/tunnel-revoke-cli.test.ts`, which fail on the previous release.
| Metric | Before | After |
|--------|--------|-------|
| DELETE /token with a pending setup key | 200, session survives | 200, all 3 tokens deleted |
| Revoked agent re-connects via leftover key | new session minted | 401 |
| `$B tunnel revoke ` | Unknown command 'tunnel' | revokes, then verifies against /agents |
| Second DELETE for the same agent | 200 again | 404 |
| Bare `--restrict` (forgotten value) | silent FULL access | hard error, exit 1 |
| Docs on default pairing scopes | "read+write, no JS" | read+write+admin+meta, stated plainly |
### What this means for you
Revoke means revoked: one command deletes the session and every setup key, prints the count, and re-reads the agent list to prove the agent is gone. `$B tunnel agents` shows everyone paired, pending setup keys included. The pairing docs now tell the truth about default access, when to reach for `--restrict` (agents reading untrusted pages), and that `$B stop` clears every token at once. Scope typos fail at `/pair` naming the bad scope instead of surfacing to the remote agent as a body error, and a scopes list can no longer smuggle in the `control` scope.
### Itemized changes
### Added
- `tunnel revoke ` and `tunnel agents` CLI subcommands: pre-server (never boot a daemon to revoke against it), post-revoke verification re-read, truthful exit codes for unknown names, unreachable daemons, and old daemons that claim success while the agent stays listed.
- `GET /agents` lists pending (unexchanged) setup keys, marked `pending`; setup-key tokens never leave the server. `DELETE /token` responses carry `tokens_deleted` and the daemon logs the count.
### Changed
- The CLI always sends an explicit scopes list; both CLI and server reference one exported `DEFAULT_PAIR_SCOPES` constant, pinned by a source tripwire so the defaults cannot drift apart again.
- The scope-denied 403 hint recommends re-pairing without `--restrict` or with `--control`; it no longer suggests `--admin`, which over-granted browser control.
- pair-agent/SKILL.md, REMOTE_BROWSER_ACCESS.md, and ARCHITECTURE.md document the real default, `--restrict`, and the tunnel allowlist nuance (`eval` works remotely; `js`/`cookies`/`storage` are local-only). The never-implemented `tunnel rotate` is replaced by `$B stop`, and the phantom `/sidebar-chat` tunnel entries are gone.
### Fixed
- `revokeToken` deletes ALL tokens for a client id: the session plus spent and pending setup keys. Closes the false-200 revoke and the re-grant hole.
- Bare `--restrict` (or `--restrict` swallowing the next flag) errors out instead of silently granting full access; `--restrict` can never grant `control`.
- Scope and rateLimit typos are rejected at `/pair` and `/token` with the field named; `rateLimit: 0` (unlimited) survives the /pair path.
- `DELETE /token/:id` decodes percent-encoded client ids, so names with spaces round-trip from the CLI.
### For contributors
- 35 new test cases: revoke-all regression shapes, a subprocess CLI harness with stub daemons pinning the version-skew net ("Revocation incomplete" on a lying daemon) and every CLI error branch, e2e scope-contract and 403-hint pins, and code-shape tripwires for `DEFAULT_PAIR_SCOPES` and the decode path.
## [1.68.1.0] - 2026-08-18
**Phantom hook errors are dead. Your settings.json now heals itself**
**on every setup, and no ephemeral path can ever be baked in again.**
If you work in Conductor workspaces or git worktrees, you have probably seen it: `PostToolUse:AskUserQuestion hook error ... No such file or directory` spraying on every question, pointing at a workspace you deleted last week. The cause was a three-part failure. Setup baked the running tree's physical path into your global `~/.claude/settings.json`, the Conductor auto-opt-in overrode the exact flag `bin/dev-setup` passes to prevent that, and the dedupe tag gstack relied on gets stripped by Claude Code itself, so every new workspace appended a fresh dead entry instead of replacing the old one.
All three are fixed at the root. Hook registration is now canonical-only: commands point at the stable `~/.claude/skills/gstack` install or are not registered at all. Ownership is decided by a fixed identity table in `bin/gstack-settings-hook`, per hook item, so it survives tag-stripping and can never claim a hook you wrote yourself. And every `./setup` run now heals first: `gstack-settings-hook prune-stale --repoint` removes dead gstack entries, re-points stale ones, restores stripped tags, and collapses duplicates, printing one line only when it changed something.
### The numbers that matter
Source: the 2026-08-17 incident on a real dev box, replayed byte-for-byte as the `incident facsimile` test in `test/gstack-settings-hook-schema-aware.test.ts`.
| Metric | Before | After | Δ |
|--------|--------|-------|---|
| Hook entries in settings.json | 11 (6 dead) | 5, all canonical | −6 dead |
| Error lines per AskUserQuestion | 4 | 0 | −4 |
| Hook processes spawned per question that do nothing | 4 | 0 | −4 |
| Traced code paths under test | — | 53 of 61 (87%) | new |
The healer also fixes damage you could not see: a corrupt settings.json is never overwritten (every mutator now fails closed instead of clobbering it with `{}`), a user-tightened 0600 file keeps its mode across rewrites (settings.json can carry API keys), concurrent setups can no longer rename a half-written temp file into place, and uninstall now cleans hooks BEFORE deleting the install root, which previously made cleanup silently no-op in exactly the case it existed for.
### What this means for you
Run `./setup` (or `/gstack-upgrade`) once and the errors stop, on every machine, with a printed receipt of what was healed and a backup beside the file. New workspaces can never reintroduce them. If you ever want everything gone, `gstack-uninstall` now actually removes every gstack hook, including the ones an older version orphaned.
### Itemized changes
### Added
- `gstack-settings-hook prune-stale [--repoint ] [--all]`: self-healing for hook registrations. Dead gstack entries pruned, stale paths re-pointed at the stable install, stripped `_gstack_source` tags restored from the identity table, exact duplicates and within-entry twins collapsed. Runs automatically at the start of every `./setup`; `--all` is the complete teardown sweep used by uninstall and `--no-team`.
- `gstack-config has `: key-presence check through the same state-dir resolution as `get` (which returns defaults for absent keys), so consent logic can tell a recorded decision from a default.
- KNOWN_HOOKS identity table covering all six gstack hooks (plan-tune trio, timeline Stop, session update, verify-gate), shared by registration dedupe and the healer so the two can never drift.
- A mutation lock around every settings.json write: mkdir-based with an owner token, ownership-checked release, and atomic stale-lock takeover. Backups get unique names and rotate (10 kept); `rollback` validates its pointer and restores atomically.
### Changed
- Hook registration is canonical-only. Setup never writes a running-tree path into global settings; if the stable install is missing a hook, it skips with a visible log line instead. The Conductor auto-opt-in for AskUserQuestion reliability hooks now respects explicit decisions (flag, env, or a recorded config key) and fires only on the true silent fall-through.
- `add-event` is the single quoting authority: registered commands are normalized once (whitespace and shell metacharacters escaped), so a spaced or `$`-bearing install path produces a working hook from the first registration. Windows gets the required `bash ` prefix on all hooks, not just SessionStart, and MSYS-form paths no longer read as dead to the healer.
- All settings.json mutators are per-item: a hook you co-located in the same entry as a gstack hook survives every gstack operation, including uninstall, and gstack never tags an entry that contains your items.
- Teardown paths (`gstack-uninstall`, `./setup --no-team`) run hook cleanup before any deletion, sweep untagged strays by identity, and keep stderr attached so a skipped cleanup is loud, never silent.
### Fixed
- Deleted Conductor workspaces and worktrees no longer leave dead hooks erroring on every AskUserQuestion, session start, and stop event.
- A corrupt settings.json is preserved and reported (exit 3) instead of being replaced with an empty object by the next hook operation.
- settings.json file mode is preserved across rewrites; fresh files are created 0600.
- Liveness checks treat only provable absence as dead, so an unmounted volume or permission blip cannot prune a working hook.
- A vacuous test in the banner-tripwire check executed its script through JSON-as-shell-quoting, silently littering a `2nelsen` artifact in the repo root on every suite run while asserting nothing; it now passes the script as argv and asserts both branches.
### For contributors
- 60+ new or updated test cases across 8 files, including the incident facsimile, a two-writer concurrency smoke, an uninstall test that runs the installed copy from inside the root it deletes, held-lock teardown visibility, quoting round-trips, and static tripwires pinning canonical-only registration, heal-first ordering, matcher-literal parity, and the shared-prelude call sites.
- The review pipeline for this release (five specialists plus red team plus two Codex passes) contributed 14 verified hardening fixes; rejected findings are documented in the PR.
## [1.68.0.0] - 2026-08-18
**The next tracker wave: 16 verified fixes in, 90 stale PRs and 21 issues out.**
**Six community contributors credited, one queue race killed for good.**
This release lands the full next-wave queue: six community PRs ported with
authorship intact, ten fixes of our own, and the six adversarial-review
residuals the last wave deferred. The headline internals: the brain-sync
queue moved to a per-record spool directory, so the enqueue/drain race class
is structurally gone, not narrowed. The session-update lock records the
process that actually holds it, heartbeats while it works, and expires on a
hard TTL, so concurrent updaters can no longer trample a live install. And a
live bug caught during this wave's own review, a stray `~/.git` directory
silently misfiling decisions and learnings into the wrong project store, is
fixed with a self-healing cache and a ten-case parity suite.
### The numbers that matter
Source: this branch vs main (`git diff main...HEAD --stat`), the wave's
coverage audit, and the tracker close-out run on 2026-08-17.
| Metric | Value |
|---|---|
| Fixes landed (issues closed by this release) | 16 |
| Community PRs ported with credit | 6 (6 contributors) |
| Open PRs closed with receipts | 90 |
| Stale issues closed with version pointers | 21 |
| Diff | 133 files, +6,276 / −649 |
| New/extended test files | 31 (coverage audit: 96% of changed surfaces at behavior+edge+error depth) |
| Review rounds absorbed pre-merge | 3 (specialist army, then two cross-model adversarial passes) |
The tracker numbers are the striking ones: 111 stale items left the queue in
one day, each with a receipt naming the release that covered it. Contributors
whose fixes were absorbed months ago now have closure with credit instead of
an open PR going quiet.
### What this means for you
If a skill ever told you the brain queue was empty while records sat in it,
or `--probe` promised thousands of pages that `--bulk` then refused, or a
second Claude session stomped your gstack update mid-pull, those classes are
closed and each one is pinned by a regression test. Update with
`/gstack-upgrade`, which itself now fast-forwards first and never discards
unpushed work without telling you exactly what it would delete.
### Itemized changes
#### Added
- `/scrape` and `/skillify` now carry the untrusted-content processing rules,
single-sourced with the browse reference so the wording can never drift.
Re-derived from PR #2612. Contributed by @Lockyer228 (#2441).
- `$B cdp` allows `Emulation.setCPUThrottlingRate` and
`Network.emulateNetworkConditions` for real perf measurement on simulated
low-end clients. Overrides persist until cleared; the justifications say so.
Contributed by @henbima (#2602).
- Transcript ingest honors the per-remote trust store: `deny` and `read-only`
remotes are skipped with per-tier counts, a corrupted store aborts before
any write, and the policy lookup is one batched subprocess for the whole
corpus (#2392).
- The gbrain source worktree advances on the daily sync, so brains stop
serving stale pages between setups. The unattended path refuses dirty
worktrees and never force-removes (#2516).
- `gstack-gbrain-repo-policy get --batch`: one spawn classifies every remote.
#### Changed
- **Behavior change:** `gstack-config get ` now exits 1 with
empty output, so `|| echo fallback` callers finally fire. Keys whose empty
value is meaningful (`cross_project_learnings`, `salience_allowlist`,
`user_slug_at_*`, `redact_repo_visibility`, `repo_mode`) still return empty
with exit 0. Scripts that relied on unknown keys silently returning empty
with exit 0 must add a fallback. Contributed by @benjaminberes-bp (#2611).
- The brain-sync queue is a maildir-style spool (`.brain-queue.d/`, one file
per record, atomic rename). Writer and drainer never share an inode; the
drain deletes only records classification proves were staged or dropped,
so a classifier crash or a malformed pulled privacy map retains everything
instead of discarding it. Legacy queues migrate on the next drain.
- `--probe` in memory-ingest counts through the same attribution and policy
gates as `--bulk`, with a bounded 256KB read per transcript, so its numbers
are the numbers. Re-derived from PR #2612. Contributed by @Lockyer228 (#2394).
- `/gstack-upgrade` fast-forwards with autostash first; the destructive
fallback runs only on a provably-clean tree with no unpushed commits, or
after an explicit confirmation listing exactly what would be discarded (#2517).
- Skill completion always reviews the session for durable learnings and says
so explicitly when there are none. Re-derived from PR #2612. Contributed by
@Lockyer228 (#2402).
- `/codex` documents the measured session-overhead reality: resume does not
amortize the prelude, so prefer one call per skill (#2387).
- MCP scope resolution is project-first everywhere, matching Claude Code's
verified precedence, and one project's remote gbrain registration no longer
reclassifies every other project on the machine.
#### Fixed
- plan-tune refuses `never-ask` on one-way question ids at write time and
reports previously-stored inert preferences in `--stats`. Contributed by
@szsunyuan (#2488).
- A typo'd `gstack-redact` subcommand exits 1 with usage instead of silently
scanning stdin (or hanging on a terminal). Contributed by @kinoko-studio.
- One ambiguous ref no longer kills the whole annotated screenshot: exact
matches stay exact, ambiguous refs fall back to first-match and are counted
visibly in the output. Contributed by @namtrok.
- `gstack-version-bump repair` refuses to write a fabricated `0.0.0.0` into
package.json when VERSION is missing or empty, while a genuine `0.0.0.0`
file still repairs. Re-derived from PR #2612. Contributed by @Lockyer228 (#2600).
- The session-update lock records the live holder (not the exited parent),
heartbeats during long pulls and setups, expires on a hard TTL so a
recycled PID cannot wedge it, and reclaims atomically with an
ownership-checked cleanup (#2613).
- `gstack-slug` resolves the canonical owner-repo slug even when a stray
marker directory sits above the repo; the poisoned-cache shape self-heals,
legitimate sticky identities are preserved, and the native Windows fallback
agrees with the shell implementation on every pinned fixture.
- `/review` checklist paths resolve from the installed skill root, so review
runs work in every target repo, not just gstack's own checkout (#2518).
- next-version's offline fallback queries live remote refs without mutating
local state, fetches unreadable claims before giving up, and never silently
reissues a sibling branch's version.
- Setup-registered hooks prefer the global install path and re-point stale
absolute paths on re-run; duplicate registrations collapse to one; a
corrupt settings.json is refused loudly instead of being replaced.
- Windows: every `Bun.spawn` in browse carries `windowsHide` with a census
tripwire, and project-scoped brains resolve on backslash paths.
#### For contributors
- 90 absorbed or superseded PRs and 21 fixed issues were closed with receipt
comments pointing at the releases that covered them; ported PRs close with
porting-commit receipts when this release merges.
- The parity-suite skeleton ceilings absorbed this wave's preamble growth
with measured notes; the referenced-path scanner self-check re-anchored to
the installed-root form.
- New follow-ups filed in TODOS.md: skillify structural isolation, slug store
migration for pre-fix data, deny retroactivity for already-ingested pages,
and the slug heal-probe cache sentinel.
## [1.67.2.0] - 2026-08-18
**Codex installs now match the model you actually run.**
**gpt-5.6-sol gets a bounded-scope profile that finishes the job, then stops.**
Every gstack skill carries a model-specific behavioral patch. This release makes that patch model-aware for Codex: `./setup --host codex` reads the top-level `model` from `${CODEX_HOME:-~/.codex}/config.toml` and renders the matching profile. The headline is `gpt-5.6-sol`. Sol reads completeness language like "exhaustive" and "Boil the Ocean" as authority to keep going, widening into adjacent cleanup and speculative hardening nobody asked for. Its new profile pins the boundary: the explicit task is the lake, adjacent findings are report-only, investigation stops once the cause is established, and the run terminates on one clean verification pass. Full coverage inside the boundary still applies, and the AskUserQuestion decision-brief format is never trimmed.
Sol is exact-match only. Terra, Luna, dated snapshots, and any suffixed ID deliberately fall back to the generic GPT profile, and the resolver warns when a near-miss like `gpt-5.6-sol-2026-08-01` lands on generic gpt.
### The numbers that matter
Source: the new periodic scope-termination eval (`EVALS=1 EVALS_TIER=periodic bun test test/codex-e2e-sol-scope.test.ts`, result in `~/.gstack/projects//evals/`) and the free suite (`bun run test`).
| Metric | Before | After |
|---|---|---|
| Codex skill overlay | one fixed profile for every install | matched to `config.toml`, `--model` per-run override |
| Sol on a planted one-line bug (live eval) | no measurement | fixed in 21 tool calls, 173s, both decoy TODOs byte-identical |
| Scope check in that eval | not measured | untracked, staged, and unstaged files all counted |
| Hermetic Codex E2E environment | whole operator `~/.codex` tree copied in | `auth.json` only, `CODEX_HOME` pinned |
| Kiro skill profile | inherited whatever the shared render held | always the claude profile, rebuilt at install time |
| Upgrade skill reinstall target | bare `./setup` (claude) for every host | the host it was generated for |
The eval row is the one to internalize: the same investigate skill that tells Claude to boil the ocean drives Sol to fix exactly one function, run the one targeted test, and stop with two tempting decoy TODOs untouched.
### What this means for Codex users
If you run Codex on `gpt-5.6-sol`, rerun `./setup --host codex` once. Your skills keep the full gstack workflow (STOP points, review gates, decision briefs) but stop sprawling into work you did not ask for. Change your Codex model later, rerun setup, and the profile follows. `--model ` overrides detection for one run and tells you how to make it stick.
### Itemized changes
#### Added
- `gpt-5.6-sol` model profile (`model-overlays/gpt-5.6-sol.md`): explicit task boundary, report-only adjacent work, bounded investigation, terminate on verified completion, AskUserQuestion format preserved in full.
- Codex model detection at setup: new `scripts/resolve-codex-generation-model.ts` reads the top-level `model` from `${CODEX_HOME:-~/.codex}/config.toml`, validates against the model allowlist, treats config values as data (control characters stripped from every surfaced string, absolute-path guard on the config location), and falls back to the generic GPT profile with a warning on unreadable or unsupported configs. `./setup --host codex --model ` overrides for that run.
- Per-host generation defaults: `HostConfig.defaultModel`, validated at generation time. Codex renders the GPT profile when no `--model` is passed; every other host keeps claude. `docs/ADDING_A_HOST.md` documents the new field.
- Periodic scope-termination E2E (`test/codex-e2e-sol-scope.test.ts`): installs the FULL generated investigate skill, plants a one-line bug beside decoy security and migration TODOs, and asserts the fix lands inside the boundary within 30 tool calls, the decoys stay byte-identical, the regression oracle survives unweakened, and nothing gets committed. Wired into the periodic eval matrix, the paid-shard globs, and diff-based selection (`codex-sol-scope-termination`).
- Sol-specific Completeness Principle and first-run intro copy: Boil the Ocean within the user's explicit task boundary.
#### Changed
- Generated upgrade skills reinstall their own host: `./setup --host codex` in Codex renders, `--host kiro` in Kiro copies (rewritten at copy time), bare `./setup` only for Claude.
- Kiro installs render the claude profile before copying skills, then restore the resolved Codex profile, so Kiro never ships GPT-family behavioral text and live `~/.codex` symlinks stay correct. The Codex skills path honors `$CODEX_HOME`.
- The hermetic Codex E2E runner copies `auth.json` only. Operator plugins, MCP servers, rules, and skills no longer leak into supposedly hermetic evals. Per-run `model`, TOML config overrides, and `--ignore-user-config` are supported.
- `setup` resolves the Codex generation model on every run (a read-only TOML lookup), so any install path preserves a Sol user's rendered profile; the codex install summary prints the active profile and its source.
#### For contributors
- New free-tier coverage: every resolver branch including hostile-config shapes (10 tests), overlay content pins, the explicit `--model` override through the real generation CLI, real-file periodic-tier classification for both codex E2E files, and invalid `defaultModel` validation.
- Static pins in `test/setup-codex-model.test.ts` hold the load-bearing setup properties: unconditional resolver, quoted `--explicit` argv, fail-closed empty-resolver exit, the Kiro claude-render sandwich, and the `--host kiro` rewrite.
- The Sol E2E snapshots the exact prior `.agents` render and restores it in `beforeAll`, so the shared tree never stays Sol-flavored for goldens, parallel shards, or symlinked installs. Fixture commits disable gpg signing so the eval runs under any global git config.
## [1.67.1.0] - 2026-08-16
**We read every line of external-contributor code from the last two months.**
**Six findings hardened, two refuted, zero backdoors.**
gstack ran an explicit security sweep over all external-contributor code merged since mid-June: the seven directly-merged `time-attack` PRs, the two fork-port squash waves, and the roughly fifty absorbed community PRs. About 38,000 lines across ~500 files, read with an adversarial eye. The verdict up front: no backdoor, no exfiltration path, no live secret leak. The contributions are net security-strengthening. This release hardens the six real findings the sweep confirmed and locks each one behind a regression test, so the property it protects holds by construction, not by luck.
The pre-push secret scanner now catches all-caps database passwords. Persisted browser sessions stay out of git whether or not your repo has a `.gitignore`. The App Store Connect key the release flow mints is scoped to the one app you are shipping, and the exit report tells you it exists and how to revoke it. The iOS test bridge's Release compile-out (shipped in v1.67.0.0) is now pinned by a free-tier tripwire that fails CI on any regression to a platform-only gate. The browser server's Node spawn shim has its `exited`/drain/memory-cap contract back. Bearer-token comparison is constant-time.
### The numbers that matter
Source: a two-wave read-only audit (72 agents, two independent verifiers per finding) plus a four-specialist pre-landing review. Reproduce the headline check with `echo "postgres://admin:${DB_PW:-PROD2026SECRET}@h/db" | bin/gstack-redact` (the shell expands the braces to the real all-caps password; exit 3) and `bun run test`.
| Property | Before | After |
|---|---|---|
| DSN with an all-caps password (`PROD2026SECRET`) at pre-push | passed the HIGH gate | HIGH block (exit 3) |
| `postgresql://USER:PASSWORD@host` doc placeholder | skipped | still skipped (pinned) |
| Persisted session cookies in a `.gitignore`-less repo | git-committable | ignored by construction |
| Minted App Store Connect key scope | every app on the team | the one app being shipped |
| iOS Release compile-out guard (shipped v1.67.0.0) | unpinned | CI tripwire on any regression |
| `await proc.exited` on the Windows Node fallback | resolved `undefined` | resolves the real exit code |
| Loopback bearer-token comparison | byte-by-byte `===` | constant-time |
The one that matters most for a public repo: opt-in browser session persistence kept live cookies and request logs under `.gstack/` inside the working tree. Now a self-contained ignore lands there at setup time, so `git add -A && git push` cannot ship them.
### What this means for you
If you run gstack from a build that pulled in community or fork-ported code, this is the release where someone read all of it and calibrated the guards against real credential shapes, not just placeholders. Run `bin/gstack-egress verify` and `bin/gstack-redact` on your own repos with confidence. The full audit trail and the governance follow-ups (a required-review rule for `main`) are captured for maintainers separately; nothing here changes a command you already run.
### Itemized changes
#### Fixed
- The pre-push credential scanner blocks a DSN whose password is a real all-caps secret (`PROD2026SECRET`-style) at the HIGH tier. The `USER:PASSWORD` documentation convention still suppresses, pinned in both directions with a table-driven test over the full placeholder set. (`lib/redact-patterns.ts`)
- The browse state directory (`.gstack/`) carries a self-contained `.gitignore` written unconditionally when the directory is created, so persisted `session-state.json` cookies and `browse-network.log` / `browse-audit.jsonl` request headers can never be committed, regardless of the project's own `.gitignore`. (`browse/src/config.ts`)
- The Node `Bun.spawn` polyfill regains its `exited` promise, eager stdout/stderr drain, and 16MB output cap, restoring correct child-process handling on the Windows Node fallback (cookie import, browser-skill children). (`browse/src/bun-polyfill.cjs`)
- The iOS QA touch bridge's Release compile-out (the `#if !defined(DEBUG)` short-circuit plus the `cSettings` DEBUG define, shipped in v1.67.0.0) is pinned by a free-tier static tripwire: any regression to a platform-only gate, a reordered guard, or a dropped define fails CI on every PR. (`test/ios-debug-bridge-release-guard.test.ts`)
- Loopback bearer-token comparison in the browse server is constant-time. (`browse/src/server.ts`)
#### Changed
- The App Store Connect upload key minted during an Apple release is scoped to the target app (`allAppsVisible:false` with an explicit `apps` relationship) instead of every app on the team, and the release exit report discloses the key and its revocation path. (`ship/sections/apple-release.md`)
- `gstack-egress verify` documents that ledger truncation and deletion are out of scope for the forensic-observability threat model. (`bin/gstack-egress`)
#### For contributors
- New regression guards pin each security property against a silent revert: a static tripwire for the constant-time `validateAuth`, a table-driven suppression test over the exported `URL_PASSWORD_PLACEHOLDER_WORDS`, an unconditional-write test for the state-dir ignore, a static tripwire for the iOS Release compile-out, and the restored `Bun.spawn` contract tests.
## [1.67.0.0] - 2026-08-16
**The tracker wave: browse survives macOS, installs are complete,**
**memory sync never drops a record. 30 contributors landed.**
This release mines the full issue tracker and community PR queue. Browse now
classifies a macOS XProtect kill at Chromium launch and heals itself. It
clears the quarantine flag, reinstalls the pinned browser revision from the
right install root, and retries, all bounded and logged. Fresh installs link
every runtime asset a skill references, so /review and friends work on a
clean machine the first time. Brain-sync's queue is drained with a classified
disposition. Privacy-held records are retained and labeled, a failed push
keeps its commit and re-delivers it on the next run, and the retry only ever
publishes commits it authored itself. Twenty-five community PRs landed with
credit, and roughly thirty-five issues close on merge.
### The numbers that matter
From the wave's gate eval run (`bun run eval:bg:gate`, log in
`~/.gstack-dev/eval-runs/`) and the free suite (`bun run test`) at HEAD.
| Metric | Before | After | Δ |
|---|---|---|---|
| Browse launch on macOS 26 (XProtect kill) | manual reinstall | classified + self-healed | automatic |
| Skill runtime assets on a fresh install | SKILL.md + sections only | every referenced asset | /review works day one |
| Brain-sync queue at a push failure | truncated | retained + re-delivered | no data loss |
| Detector push with an interleaved user commit | published it | refuses | author boundary holds |
| Gate evals | 41/43 | 43/43 | both reds root-caused |
| Free suite | — | ~7,000 tests, ~90-100s | green at HEAD |
The brain-sync row is the one to internalize: the queue is only ever rewritten
by subtracting the exact records that were staged, against a live re-read, so
a record enqueued mid-drain survives to the next boundary.
### What this means for gstack users
Upgrade and the three most-reported failure classes disappear: browse comes
back on macOS without touching a terminal, a teammate's first `./setup`
produces working skills, and your cross-machine memory stops silently thinning
under flaky networks. If you filed one of the ~35 issues this closes, your
repro is now a regression test with your name on the commit.
### Itemized changes
#### Fixed — the three P0s
- **Browse dead on macOS (#2554).** Playwright pinned to 1.62.1 (split from
dependabot #2582), plus an XProtect kill-signature classifier with positive
AND negative fixtures, a one-shot quarantine-clear + bounded (~120s,
process-group-killed) reinstall from the gstack install root that pins the
matching Chromium revision, structured heal logging, and an upgrade-time
quarantine-clear + reinstall in `setup` for already-poisoned caches. The
heal resolves the install
root via `os.homedir()` and keeps its manual-remediation guidance even when
the post-heal retry fails.
- **Fresh installs missing runtime assets (#2317, #2454).** `setup` links
every runtime asset with an explicit exclusion list (node_modules, dist,
*.tmpl, test, hidden), pinned by a two-class referenced-paths test:
alias-relative references must exist under the installed alias, repo-anchored
ones in the tree modulo a reasoned dist/ allowlist.
- **Brain-sync data loss (#2549).** Queue records are classified at drain
time: skip-filtered and nonexistent drop WITH counts (full paths in a 0600
sidecar), privacy-held records are retained and labeled instead of being
wiped as "no allowlisted changes", unparseable lines are preserved, and the
rewrite subtracts the staged set from a LIVE re-read so concurrent enqueues
survive. A failed push keeps its commit; a run-start detector re-delivers it
— receipted, locked, throttled to one attempt per 10 minutes, bounded by
git's low-speed limits (portable to stock macOS), and gated to fire only
when EVERY unpushed commit is its own, so an interleaved manual commit in
~/.gstack is never auto-published. The sync lock releases on every exit
path, including interrupts mid-push.
#### Fixed — browse & daemon lifecycle
- A healthy daemon is never killed by `browse start` (the #2219 iron rule):
a total-budget health probe answers in ~8s, busy daemons get "retry or
--force-restart" plus a nonzero exit, and only an explicit `--force-restart`
ever kills an alive pid — pinned by a regression test. `browse stop` on a
dead daemon short-circuits to success (#2254); `/gstack-upgrade` defers to a
busy daemon and prints the escape hatch (#2551).
- Chromium no longer dies with the terminal: signal handling moved off
Playwright's defaults at all three launch sites with a SIGHUP handler
routing through the real shutdown path, and a tripwire pinning the count.
- The terminal-agent allocates from the same fixed port range as the daemon
(#2314) — and that range now ends at 49151, actually below the macOS
ephemeral pool it exists to avoid; boot retries a raced bind instead of
dying. Windows terminal-agent leaks fixed via `process.kill(pid, 0)`
liveness (#1952) and the error-handling helpers. Contributed by @SYKhayyat
(#2414).
- Daemon crash logs persist without tokens or unsanitized page content
(needle-tested). Contributed by @phuttimatebenchanakatkul (#2461).
- The dead security-shield surface was removed end to end (−272 net lines) while
the live L4 sidecar path keeps its status endpoint — docs updated in the
same commit. Contributed by @frederik-kaster-noygear (#2557, with the
pipe-capture core from #2559). CDP `Emulation.setEmulatedMedia` joins the
allowlist — contributed by @meshailabs (#2419). Windows gbrain probe
timeout — contributed by @vaston-viji (#2450). `browse/dist` mkdir —
contributed by @guyua9 (#2542).
- First `patchedDependencies` entry: playwright-core's two Windows spawn
sites carry `windowsHide` (#2160, #1989), statically pinned and
independently revertable.
#### Fixed — install & setup correctness
- Root-alias skills install as rewritten copies, never symlinks whose edits
would corrupt generated sources (#2511, #2201). Windows re-runs refresh
real-directory installs (#2444), and uninstall deletes only directories
that pass BOTH the inventory match and the generated-banner provenance gate,
listing (never deleting) anything else (#2563).
- `--host cursor` gets the full install slice — contributed by @szsunyuan
(#2547). Settings-hook dedup includes the command (#2382) — contributed by
@gregario (#2431). `:user` renders route through `--out-dir` (#2569) with a
migration that cleans legacy in-place render dirt. setup-gbrain invocation
paths fixed (#2250) — contributed by @SomSamantray (#2409). Office-hours
installs into codex/factory/opencode runtime roots (#2449).
- The redact pre-push hook stays opt-in but its fail-open gaps are closed,
with a one-time consent prompt (#1946). Skills-timeline Stop hook ships
fail-open (always exit 0, 2s budget) with setup registration (#2553).
- iOS QA: DebugBridgeTouch compiles out of Release builds — contributed by
@Bastea (#2585); front-most bridge ordering — contributed by @IDSTUK
(#2397); compat preflight docs — contributed by @itstimwhite (#2581).
#### Fixed — memory & gbrain
- Windows slug resolution and the decisions.jsonl allowlist (#2396) —
contributed by @source-utsho (#2561). Brain-sync arithmetic-injection
guard — contributed by @sneakygriff (#2588). Windows bash routing for
brain-sync/gbrain — contributed by @ShahriarLak (#2510), extended to every
gbrain-sources spawn (#2471). `--full` walks the full tree — contributed by
@ShahriarLak (#2406). Honest "missing" from brain-cache — contributed by
@sneakygriff (#2587). Memory-ingest parses both codex rollout shapes and
stages outside GSTACK_HOME (#2105, #2104).
- gbrain detection: engine-locked is a healthy status (#2456), bearer-token
thin clients are recognized (#2520), GBRAIN_HOME gets its .gbrain segment
(#2521), project-scoped MCP registrations are honored (#2499). Source pins
respected — contributed by @exGeni (#2417); `--dry-run` works offline
(#2536) — contributed by @CarringtonCreative (#2540); bun-on-npm PATH
guidance (#2487); dream-stage classifier anchored (#2341).
#### Fixed — version tooling, diff-scope, redaction
- VERSION stays the 4-digit source of truth; package.json carries the
npm-valid 3-digit translation, lockfiles sync only when they already exist,
and drift is judged on translated forms. Built on re-derived work
contributed by @YiftahR (#2501), @ortonom (#2568), and
@CarringtonCreative (#2531, #2545). Pinned repos compare base and current
against the SAME file (#2462). JSON version-paths get honest per-file
recovery messages. The path pins (`.gstack/version-path`,
`.gstack/package-json-path`) cannot escape the repository — absolute paths,
`..` traversal, and symlink escapes are all refused, and a lockfile
symlinked outside the repo is skipped with a warning.
- Diff-scope covers api/*, migrations/*, and db/data, with a no-match exit
code and uncommitted-work handling (#2526, #2455, #2299). Redact scans the
merge-base range and knows parcel IDs are not phone numbers — contributed
by @Two-Six-Alpha-1115 (#2592, #2591); rebased force-pushes are scanned
correctly, proven by test (#2573).
- The codex model probe caches its verdicts both ways: a working model for an
hour, a deterministic model-400 for 15 minutes (editing config.toml
re-probes immediately) — so the affected account stops paying a 30s round
trip per review section (#2477). Its timeout wrapper now enforces the
deadline with a bash-native watchdog on stock macOS, where no timeout
binary exists.
#### Fixed — templates & everything else
- Skills running under Codex skip the nested codex specialist with a printed
notice (#2519). Codex web-search flag unified behind one resolver constant
across 19 sites (#2525). Slugs are sanitized in every path position
(#2550) — with groundwork contributed by @harjothkhara (#1851). AGENTS.md
routing probe — contributed by @gamerey43 (#2500); empty-find fallthrough
killed — contributed by @tranthanhnhatkhoa (#2483); cygpath MSYS builds —
contributed by @chiragborse1 (#2452). /ship's review army loops until clean
(#2391). Question-registry path is absolute (#2489). Retro glob (#2552),
capability-check temp file (#2503), repo-mode stat order (#2195), hover doc
note (#2445), make-pdf boolean flags — including `--strict` and
`--confidential` — no longer swallow the input file, with a guard test that
derives the flag set from the source (#2514).
#### For contributors
- Test/generator infra hardened first: host-config golden isolation (#2532),
hermetic-wiring tripwire and YAML ellipsis quoting — contributed by
@sneakygriff (#2586, #2589); prepush PATH separator — contributed by
@luckywenapere (#2544); gen-skill-docs throws on duplicate preamble tokens.
- Dependency hygiene: puppeteer-core removed outright (zero consumers),
adm-zip CVE closed via lock override — contributed by @anupamme (#2485);
transformers/marked/socks bumped with the ONNX sidecar smoke green;
.gitattributes LF pin — contributed by @mlaniak (#2527); GitHub Actions
bumps — contributed by @dependabot (#2594).
- The wave's own adversarial reviews (Codex + Claude, 28 findings) landed as
fixes in-branch; verified residuals are filed in TODOS.md with rationale.
## [1.66.1.0] - 2026-08-16
**Every claim gstack makes now binds to the content it was made on.**
**Tracker text is data. Guard hooks actually guard.**
Reviews and test results used to be prose claims: "review is recent" meant a commit-count guess that a rebase could crash, and "tests passed" meant trusting output from a tree that may have changed since. Both now carry a working-tree content fingerprint (`bin/gstack-wtree`, ~0.2s). A review of identical content grades CURRENT through rebases, amends, and squashes. A test run recorded by the new `bin/gstack-evidence` ledger stays citable at /ship's verification gate only while the content is byte-identical (release files carve out), the command hash matches, and nothing edited the tree mid-run. /ship and /land-and-deploy cite fresh evidence instead of re-running, and re-run live when anything moved.
PR bodies, PR comments, and model-judged issue titles now enter agent context only through a trust envelope (`bin/gstack-issue-guard`): content is data even when clean, injection-shaped lines get labeled through fullwidth and invisible-character evasion, forged envelope banners are defused, and a CI scanner fails the suite on any raw tracker-text read at all 8 ingress points. Write-backs keep a raw artifact so envelope markup can never reach a live PR.
/freeze now fails closed: unparseable payloads, quote or newline paths (the deny used to silently no-op on them), boundaries with spaces, symlinks pointing outside the boundary, and a broken install all block instead of passing. /careful gains a hard-deny tier for `rm -rf /`-class deletes and force-pushes to the default branch — including the flag-less `git push origin +main` form and quoted or refspec targets — plus additive-only custom warn patterns that can never weaken the built-ins.
### The numbers that matter
Measured on this branch; re-run with `bun test`, `time bin/gstack-wtree`, and the commands in each bin's header.
| Metric | Before | After | Δ |
|---|---|---|---|
| Review staleness on rebased/amended identical content | crash or STALE | CURRENT | correct |
| "Tests passed" binding | none (prose) | content fingerprint + command hash + max-age | new |
| Tracker-text ingress points enveloped | 0 | 8, CI-scanner enforced | new |
| /freeze deny on hostile/edge paths | silent no-op | blocks, fail-closed | fixed |
| Working-tree fingerprint cost | — | ~0.09s warm (stat-cache seeded, 40x vs naive) | new |
| Adversarial findings fixed pre-merge | — | 50 (4 specialists + red team + fresh-context pass), 6 critical | — |
The fingerprint survives commits of identical content, so the common flow — test on a dirty tree, commit, ship — keeps its evidence valid, while one untracked new source file invalidates it.
### What this means for you
/ship stops re-running suites the content already proved green and stops trusting suites the content has outgrown — the IRON LAW is now a mechanical check, not an honor system. A hostile PR comment can no longer speak to your agent with authority, and /guard's boundary actually holds on the paths where it used to silently fail. Nothing to configure: the bins ship wired into /ship, /land-and-deploy, /review, /spec, and /document-release.
### Itemized changes
### Added
- `bin/gstack-wtree` — working-tree content fingerprint (temp-index, stat-cache-seeded; identical hash to a full re-hash at ~40x less cost).
- `bin/gstack-evidence` — verification-evidence ledger: `run` wraps any command transparently (exit code always passes through; 0600 per-run logs with 2MB cap and 30-day prune; HIGH credentials in commands stored redacted; mid-run tree edits void the fingerprint) and `check` grades FRESH/STALE/MISSING per label with `--expect-cmd`, `--max-age`, and `--allow-paths` binding.
- `lib/tracker-guard.ts` + `bin/gstack-issue-guard` — trust envelope for tracker text: envelope-always, detection-only NFKC + full Unicode format-character sweep, banner-forgery defusal, no-envelope-on-fetch-failure, numeric argv validation.
- `/careful` HIGH tier (hard deny: root/home recursive deletes incl. `--no-preserve-root` and `/*` forms; default-branch force-pushes incl. plus-refspec, refspec-colon, and quoted targets; simple commands only, `--force-with-lease` never matches) and additive-only project warn patterns (`~/.gstack/careful-patterns.txt`, per-project variant).
- CI wiring scanner (`test/tracker-guard-wiring.test.ts`) failing the suite on raw tracker-text reads outside the guard, with reasoned, liveness-checked exemptions; template-drift tripwire pinning the grading rules and the write-side banner tripwire.
### Changed
- Review records (`bin/gstack-review-log`) stamp `commit_full`/`tree`/`dirty`/`wtree` authoritatively — caller-supplied binding fields are ignored; `bin/gstack-review-read` emits `---WTREE---`/`---TREE---`/`---DIRTY---`; the /ship dashboard and /land-and-deploy grade diff-scoped reviews content-first (plan-tier reviews keep time-based logic), and a rebased-away commit grades UNKNOWN instead of erroring.
- /ship Step 5 test lanes run wrapped with per-lane labels and per-run logs (no shared /tmp collisions between concurrent ships); Step 16 and /land-and-deploy 3.5b check the ledger first and cite fresh evidence, advisory-never-blocking.
- /document-release PR/MR body updates use a two-artifact flow (enveloped copy for reading, raw copy for the splice-and-write-back) with a banner tripwire that compares against the fetched original.
- /spec issue-title dedupe reads titles through the envelope and distinguishes pipeline failure from zero matches instead of silently skipping.
### Fixed
- /freeze: five boundary defects — deny JSON silently no-oped on quote/newline paths, internal spaces in the boundary path were stripped (space-bearing project dirs could never match), symlink final components weren't resolved (in-boundary symlink wrote outside the boundary), the JSON extractor truncated at escaped quotes and failed open, and a missing helper file passed edits through instead of blocking.
- /careful and /freeze now share one JSON extractor and one analytics writer (both honor `GSTACK_HOME`), ending the two-copy drift that let one hook keep a bug the other had fixed.
### For contributors
- `test/helpers/scratch-repo.ts` — shared hermetic git fixture (identity pinned, gpg signing disabled so fixture commits never invoke the operator's gpg-agent) and a PATH `gh` shim for exercising real gh success/failure branches.
- ~150 new tests across six files, including the keystone case: evidence recorded on a dirty tree stays FRESH after committing the exact tested content.
## [1.66.0.0] - 2026-08-15
**The full ~7,000-test suite in about 90 seconds, verified honest.**
**Paid evals now bill by diff, not $38 flat.**
`bun run test` used to take 454 seconds. It now runs as up to six concurrent shard processes and finishes in about 90 to 100 seconds, under a strict output contract: a shard that exits without bun's own terminal summary line is a failure, a wedged shard is killed at a size-scaled deadline and named in the epilogue, and the console shows only what you need (per-shard status, then `✗ file — test name` for anything red, full stream in a per-run log, `--verbose` for the firehose). Twelve test files that ran under no script and no CI are wired in. A 3,372-line dead eval monolith is deleted, with four never-run tests revived out of it.
Paid evals select by diff. Edit one skill and the runner executes only the shards your change touches, reports the rest as skipped-by-diff, and prints the reason. Selection sees uncommitted and untracked work, fails closed with a named cause on git errors, and an edit to the selection data itself re-runs only the changed keys instead of forcing the full suite.
### The numbers that matter
Measured on this branch. Re-run with `time bun run test` and `bun run eval:select`; eval receipts live in `~/.gstack-dev/evals/`.
| Metric | Before | After | Δ |
|---|---|---|---|
| Free suite wall clock (~7,000 tests) | 454s | ~90-100s, strict-verified | ~4.7x |
| Free-test files with Linux CI coverage | 0 | ~420, as a required PR check | new |
| Paid cost of a one-skill edit | ~$38 (full suite) | 4 of 45 shards, $0.67 | ~57x |
| Slowest CI eval job | 741s, one serial file | three jobs, each under ~250s | ~3x |
| Paid retry amplification | `--retry 2`, +84% measured | `--retry 1` | half |
The $0.67 row is a live rehearsal, not a projection: a scratch edit to `qa/SKILL.md.tmpl` selected 17 of 177 tests, ran 4 of 45 shards, skipped 41 by diff, and the /qa E2E passed.
### What this means for contributors
Runs you used to schedule around now fit inside a thought. `bun run test` before every commit is a real habit again at ~90 seconds, red names the exact test, and green means every file actually ran. Fork PRs get true test signal from the new secretless Linux lane. Ship a change and the eval bill tracks your blast radius.
### Itemized changes
### Added
- Linux free-tests CI lane (`.github/workflows/free-tests.yml`): the whole free suite on every PR and every push to main, required from day one, zero secrets, least-privilege token, failure logs uploaded as an artifact, wiring pinned by `test/free-tests-workflow-wiring.test.ts`.
- Diff-based paid-shard selection: parent-side skipping with a `skipped-by-diff` taxonomy and a selection banner naming the reason (`scripts/test-paid-shards.ts`).
- Map-diff selection for the selection data itself: editing `test/helpers/touchfiles-data.ts` re-runs only added/changed/retiered keys (old version evaluated via `git show` + a bun child; adversarial fixtures in `test/touchfiles-map-diff.test.ts`).
- Selection unions committed, staged/unstaged, and untracked changes; git failures throw naming `EVALS_ALL=1` (fail closed), and non-ASCII filenames select correctly (`core.quotePath=false`).
- `test/helpers/skill-fixture.ts`: E2E fixtures extract the SKILL.md sections a test needs instead of copying 1,800-line files — nine fixture sites cut 58-97%.
- `GSTACK_EVAL_MODEL_JUDGE` env override for the LLM-judge model; eval model resolution centralized in `lib/eval-model.ts` with per-kind `GSTACK_EVAL_MODEL_` overrides.
### Changed
- Free suite architecture: N concurrent shard processes (serial within each); tree-mutating tests and tree-measuring ratchet readers run in one serial shard after the parallel phase, so measurements never race regeneration. Shard curation lists are pinned against the live file census, and wall deadlines scale with shard size.
- Agent SDK capture default Opus → Sonnet (D1a). The judge default stays Sonnet: a live A/B on the health rubric scored Haiku 2/2/2 against Sonnet's 4/3/4, so the downgrade was pinned back per D1a's regressor clause (receipts in `test/helpers/llm-judge.ts`).
- Four expensive posture tests demoted gate → periodic (D2a).
- Paid runners: `EVALS_JOBS` (shard process count) split from `EVALS_CONCURRENCY` (within-shard), `--retry 1` on every retry-bearing paid path, one preflight API ping per run instead of ~30, detach timeouts floor-enforced against the live shard census by `test/eval-detach-timeout-floor.test.ts`.
- CI: eval Docker image cache keyed on Dockerfile + bun.lock so version bumps stop rebuilding it; Bun 1.3.13 in the image; `skill-e2e-review` split into three matrix shards; actionlint runs a digest-pinned prebuilt image; five single-core jobs right-sized; lint and skill-docs stop double-running every PR commit; the Windows lane caches bun installs and runs the curated suite instead of a hand list.
- Skill-routing E2E fixture installs skill heads, not ~18 full SKILL.md files.
### Fixed
- Ctrl-C actually cancels a run: the signal forwarders now schedule the parent runner's own exit and both shard pools stop launching new work on `SIGINT`/`SIGTERM` — previously the parent killed the current child and kept spawning API-burning shards.
- The intermittent whole-suite wedge: `browse/src/browser-manager.ts` `close()` captures the Chromium child before the close race and SIGKILLs it when graceful close times out, with unit coverage of the fallback.
- The strict-output classifier keeps stdout and stderr line assembly separate, so interleaved pipe chunks cannot hide a failure line or fake a truncation. Windows shard kills take the whole process tree (`taskkill /T`) instead of orphaning grandchildren.
- Redaction calibration: `${var}` template interpolations and ALL-CAPS `USER:PASSWORD` doc placeholders no longer block pushes, while a bare `$word` password and a literal lowercase `password`/`pass` at the URL-password position still do; the two connection-string validators share one helper so they cannot drift.
- Supabase pooler DSNs percent-encode the password segment, `wait --timeout` rejects non-numeric values instead of polling forever, response-body read failures retry as transport errors, and the CLI entrypoint lets stdout drain before exiting.
- The paid-suite preflight fails fast on a missing `claude` binary, a spawn error, or a timeout — outages surface once in the parent instead of once per shard.
- Same-name branches from different forks can no longer cancel each other's CI runs (concurrency groups key on PR number across the free, eval, and Windows lanes).
- Selection integrity: the `touchfiles.ts` facade, `e2e-helpers.ts`, and `paid-test-set.ts` are global touchfiles (an edit to selection-path code can never select zero tests); duplicate touchfiles keys fail the suite; rehomed E2E files list themselves in their own dependency maps; retro E2E passes require the report on disk.
- The intermittent context-save-list eval test that had never passed in 26 recorded runs now passes.
- `variants-retry-after` HTTP-date flake; watchdog E2E 22.7s → 1.5s; supabase-provision tests 16.5s → 0.45s via an in-process TS port.
- `package.json` version drift against VERSION.
### For contributors
- `test:gate:sharded` / `test:periodic:sharded` run tiers through the sharded paid runner; `eval:bg:*` wrap runs in `gstack-detach` with a per-tier watchdog and the machine-wide `gstack-evals` lock.
- Five pre-existing environment failures quarantined individually with in-file receipts; two dead-architecture security contract tests deleted.
- `test/e2e-tier-alignment.test.ts` enforces tier declarations and fails fatally when a sharded-runner mapper cannot see a gate file.
## [1.65.0.0] - 2026-08-14
**/autoplan, /codex on macOS, and memory ingest work again.**
**And every consent gate now means what it says.**
This is the second and final wave of the GStack 2 fork port. Wave one (v1.63.0.0)
took the audit infrastructure; this wave takes the fixes and the features. Three
skills that failed silently on every run now work: /autoplan's task aggregation
emits real tasks instead of zero, /codex creates its temp files on macOS instead
of dying on BSD mktemp, and memory ingest actually imports pages on current
gbrain builds, and prints the count so you can see it. On top of that: your
browser auth can now survive a daemon restart, /ship can take an iOS app from
working tree to Submit for Review, and four supply-chain gates now run on every
PR. Nearly all of it traces back to Sina Matian's time-attack/gstack fork, ported
with tests and attribution.
### The numbers that matter
Source: this branch (`git log 1.63.0.0..HEAD`, `git diff main...HEAD --stat`,
`bun test`), plus the GitHub issues the release closes.
| What | Before | After |
|------|--------|-------|
| /autoplan Phase 4 task output (#2018) | 0 tasks, every run | every task |
| /codex on macOS (#2091) | broken on every install | works |
| Memory ingest on gbrain 0.42+ (#2144) | 0 pages, reported success | full corpus, count printed |
| Headed browse on macOS 26 (#2242) | GPU crash, poisoned cache | launches, heals old caches |
| Auth after a browse daemon restart (#778) | logged out | restored (opt-in) |
| CI secret scanning on PR diffs | none | every PR, fail-closed |
| GitHub issues closed | | 24 |
| Community PRs landed with authorship | | 4 |
The stark one is the first three rows: those aren't degraded features, they were
features returning empty results with a green checkmark. If you ran /autoplan in
the last two months, the task list it handed off was empty and nothing told you.
### What this means for gstack users
Run /autoplan and the pipeline hands real tasks to the executor. Run /codex on a
Mac and it just works. Set `BROWSE_PERSIST_STATE=1` and a daemon restart no
longer logs you out of every site. If you ship an iOS app, `/ship` now knows the
whole App Store journey, session-minted upload keys, the price-schedule API that
replaced the broken fastlane path, error -22938 triage, one authorization moment
instead of five. Upgrade with `/gstack-upgrade`; the migration cleans any
Chromium bundle an older gstack broke and verifies the replacement download
before it claims success.
### Itemized changes
#### Added
- **Opt-in browser session persistence** (#778, #2193): `BROWSE_PERSIST_STATE=1`
snapshots cookies and tabs (atomic writes, 0600, never page HTML or
ownership), restores them off the boot path on the next start, and quarantines
a corrupt snapshot instead of crashing. Portions from time-attack/gstack.
- **Apple App Store release journey for /ship**: `ship/sections/apple-release.md`
loads before the repo-landing gates when the target is an Apple app. Encodes
session-minted App Store Connect keys, `appPriceSchedules` over the broken
fastlane `price_tier`, expanded age-rating attributes, -22938 classification,
and a one-authorization-moment flow. Refined across 21 live releases on the
fork. Portions copyright Sina Matian, MIT.
- **Code-intelligence provider contract, Phase 1**: `gstack-code-intelligence`
wraps GBrain, Sourcebot, and Graphify behind one interface with an ask-once
indexing offer for large repos (1,000+ tracked files, decline persisted).
Consent is explicit per repo (`consent yes|no`), the per-repo trust
policy's deny and read-only tiers veto write-class operations no matter what
consent was recorded, and every off-machine send writes an egress receipt that
records the consent state actually checked. Portions from time-attack/gstack.
- **Supply-chain CI**: a quality gate that runs `bin/gstack-redact` over every
PR diff (HIGH findings fail, MEDIUM annotates), dependency review on
lockfile changes, weekly OSV scans, grouped dependabot updates, and an
evidence-bar PR template. Every third-party action in the new workflows is
pinned to a commit SHA.
- **Third-party web-actions contract** in tier-2+ skills: when a workflow needs
a vendor-site step (API key signup, OAuth app), gstack offers to drive the
browser itself, hands credentials and CAPTCHAs to you, stores secrets
owner-only, and verifies with a read-only call before claiming success.
- **Design docs land in your repo** (#703, #2000): office-hours writes
`docs/designs/.md` as a concise decision record (one bullet per
decision with its why), redaction-scanned before anything touches your git
history. Plan reviews prefer the repo-local doc when both exist.
- **`gstack-verify-gate`** (opt-in Stop hook): blocks turn-end until the
CLAUDE.md-declared verify command passes. A command runs only after you trust
it once per repo (`--trust`), re-trust is required when it changes, every
grant is audit-logged, and re-entries re-run the check instead of waving it
through.
- **"Never show me these again"** for the founder-resources pitch (#538): the
opt-out verifies its own config write before promising anything. Re-enable
with `gstack-config set founder_resources true`.
- **Claimed limitations need evidence**: every tier-2+ skill now treats "the
API can't do this" as a material claim requiring the verbatim error, the
documented statement, or a live probe, and runs the ten-second check before
declaring anything blocked.
#### Fixed
- **/autoplan Phase 4 emitted zero tasks on every run** (#2018): a jq context
rebind dropped every aggregated task; the error was hidden by stderr
suppression. Six-fixture regression suite pins it.
- **/codex was broken on every macOS install** (#2091): BSD mktemp rejects
suffixed templates; all temp files now use portable templates and a static
test bans the pattern repo-wide.
- **Memory ingest imported nothing on gbrain 0.42+** (#2144): the staging dir
sits under a gitignored tree, so git-aware collectors saw zero files. Fixed
with `--include-gitignored` (community PR #2560) plus a `GIT_CEILING_DIRECTORIES`
second layer, Windows-safe, and a loud ingested-page count.
- **Headed mode on macOS 26** (#2242, #2138, #2139): gstack no longer rewrites
the signed Chrome-for-Testing bundle (the rebrand broke its code signature;
GPU processes refused to start). Launch self-heals poisoned caches, on both
headed entry points, by removing the whole revision directory so the re-fetch
actually re-downloads, and the upgrade migration does the same for existing
installs, verifying a working Chromium exists before recording success.
Branding stays on the GStack Browser wrapper app.
- **`browse stop` restarted the daemon it was told to stop**: the CLI now gets
an acknowledgment before shutdown, and the shutdown snapshot has a hard
deadline so a wedged page can never hold the port.
- **Session cookies from internal networks never reach a restored browser**:
the restore-time hygiene filter drops loopback and link-local IP literals
(127.0.0.1, ::1, 169.254.*) alongside localhost and *.internal, shared by
both the persistence path and `state load`.
- **ios-qa stopped handing out raw bearer tokens**: `/auth/sessions` returns
salted-hash token ids with revoke-by-id support, the boot token left os_log
entirely, and the IPv4 listener pins to loopback at the socket.
- **make-pdf's no-network promise holds against obfuscation**: `