diff --git a/.github/workflows/evals-main-backstop.yml b/.github/workflows/evals-main-backstop.yml index b802e1db6..da91abc02 100644 --- a/.github/workflows/evals-main-backstop.yml +++ b/.github/workflows/evals-main-backstop.yml @@ -7,6 +7,11 @@ name: Main backstop gate # backstop writes the freeze record gstack-ship-queue reads. Inert (no paid # work, nothing written) unless main's .gstack/ship-policy.json declares a # `train` block; without one the per-PR gates cover main as before. +# +# The verdict is published on the commit as the main-backstop check run +# (success when a green train covers the tree or the backstop gated it green, +# neutral when the push was superseded or deferred, failure when it froze the +# queue or could not finish); the weekly release reads it (docs/releases.md). on: push: branches: [main] @@ -25,6 +30,8 @@ jobs: if: github.actor != 'dependabot[bot]' # Settle window (60) + the full gate (270) + margin. timeout-minutes: 345 + outputs: + action: ${{ steps.gate.outputs.action }} permissions: # Pushes the freeze record to refs/notes/gstack-trains. contents: write @@ -56,10 +63,38 @@ jobs: bun-version: 1.4.2 - name: Gate main unless a green train covers its tree + id: gate if: steps.policy.outputs.train == 'true' env: GH_TOKEN: ${{ github.token }} GSTACK_STATE_ROOT: ${{ runner.temp }}/gstack-state run: | bun --no-env-file --config=/dev/null --no-install run scripts/train-backstop.ts run --repo "$GITHUB_REPOSITORY" --branch "$GITHUB_REF_NAME" --sha "$GITHUB_SHA" \ - --summary "$GITHUB_STEP_SUMMARY" + --summary "$GITHUB_STEP_SUMMARY" --outputs "$GITHUB_OUTPUT" + + # The per-commit verdict the weekly release reads (scripts/release-cut.ts). + # Its own job with checks: write and no repository code (the same split as + # evals.yml's train-check): only the action word the gate step recorded. + backstop-check: + runs-on: ubuntu-24.04 + needs: backstop + if: ${{ !cancelled() && needs.backstop.outputs.action != '' }} + timeout-minutes: 5 + permissions: + checks: write + steps: + - name: Publish the main-backstop check run on this commit + env: + GH_TOKEN: ${{ github.token }} + ACTION: ${{ needs.backstop.outputs.action }} + run: | + case "$ACTION" in + covered|gated) conclusion=success ;; + inert|superseded|deferred) conclusion=neutral ;; + frozen|failed) conclusion=failure ;; + *) echo "::error::unknown backstop action '$ACTION'"; exit 1 ;; + esac + gh api "repos/${GITHUB_REPOSITORY}/check-runs" -X POST -f name=main-backstop -f head_sha="$GITHUB_SHA" -f status=completed -f conclusion="$conclusion" \ + -f details_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + -f "output[title]=Main backstop: ${ACTION}" -f "output[summary]=${ACTION}: see the backstop job summary of run ${GITHUB_RUN_ID}" >/dev/null + echo "- main-backstop: ${conclusion} (${ACTION}) on ${GITHUB_SHA}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/free-tests.yml b/.github/workflows/free-tests.yml index 65d14b08f..948b597ee 100644 --- a/.github/workflows/free-tests.yml +++ b/.github/workflows/free-tests.yml @@ -146,7 +146,7 @@ jobs: resolved="$(realpath "$tmp")" echo "temp root: $tmp -> $resolved" test "$resolved" != "${tmp%/}" - files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts) + files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts browse/test/proxy-env-daemon.test.ts browse/test/proxy-direct-launch-args.test.ts) for file in "${files[@]}"; do test -f "$file"; done bun test "${files[@]}" diff --git a/.github/workflows/version-gate.yml b/.github/workflows/version-gate.yml index 5f8157d96..32b36a04f 100644 --- a/.github/workflows/version-gate.yml +++ b/.github/workflows/version-gate.yml @@ -26,6 +26,9 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} + # The head's release_tool runs inside the gate (lib/version-gate-train.ts); + # no token may sit in the checkout it stages. The base fetch below is public. + persist-credentials: false # The gate's own code comes from the PR's merge commit (github.sha): a PR # that edits the gate runs its edit, and an open PR picks up a gate fix diff --git a/.github/workflows/weekly-release.yml b/.github/workflows/weekly-release.yml new file mode 100644 index 000000000..bb4566aef --- /dev/null +++ b/.github/workflows/weekly-release.yml @@ -0,0 +1,114 @@ +name: Weekly release +# Tags the newest green commit on main as v, publishes the GitHub +# Release, and fast-forwards refs/heads/stable to it (docs/releases.md). Team +# installs follow stable, so this is the only thing that moves them. Runs +# every Monday morning Pacific; run it by hand with a ref for a hotfix, or +# with dry_run to see the plan and change nothing. A pull request that touches +# the release tooling runs the dry run against main with a read-only token, +# which is the only pre-merge exercise of the real workflow. The candidate's +# gate must already be green (free-tests and main-backstop check runs +# succeeded, every check run finished and none red, no active freeze): this +# workflow never runs tests or paid evals. Dispatch it from main: the cut job +# runs only there, with main's tooling. +on: + schedule: + # Monday 14:00 UTC: 07:00 PDT, 06:00 PST. + - cron: '0 14 * * 1' + workflow_dispatch: + inputs: + dry_run: + description: 'Print the plan (candidate, tag, stable move, notes) and change nothing' + type: boolean + default: true + ref: + description: 'Hotfix: release this commit of main instead of the newest green one (full 40-hex SHA)' + type: string + default: '' + pull_request: + paths: + - '.github/workflows/weekly-release.yml' + - 'scripts/release-cut.ts' + +# One cut at a time; a second dispatch waits for the first. A pull request's +# dry run is its own group. +concurrency: + group: weekly-release-${{ github.event.pull_request.number || 'cut' }} + cancel-in-progress: false + +permissions: {} + +jobs: + cut: + name: Cut the release + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + # The tag, the stable branch and the release. + contents: write + # Reads the candidate's check runs. + checks: read + steps: + # The dispatched ref supplies the tooling; the release candidate always + # comes from origin/main, which fetch-depth 0 brings along with every + # branch. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + + - name: Fetch stable and the tags + run: | + set -euo pipefail + git fetch --quiet origin '+refs/heads/stable:refs/remotes/origin/stable' 2>/dev/null || echo "origin has no stable yet: this cut creates it" + git fetch --quiet --tags origin + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Plan and cut + env: + GH_TOKEN: ${{ github.token }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && 'true' || 'false' }} + RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} + run: | + set -euo pipefail + if [ -n "$RELEASE_REF" ] && ! [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::ref must be the full 40-hex SHA of a commit on main, not '$RELEASE_REF'" + exit 1 + fi + args=(--repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY") + [ "$DRY_RUN" = true ] && args+=(--dry-run) + [ -n "$RELEASE_REF" ] && args+=(--ref "$RELEASE_REF") + echo "## Weekly release ($([ "$DRY_RUN" = true ] && echo 'dry run' || echo 'cut'))" >> "$GITHUB_STEP_SUMMARY" + bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts "${args[@]}" + + dry-run: + name: Dry run against main + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + checks: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + + - name: Plan only + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git fetch --quiet origin '+refs/heads/stable:refs/remotes/origin/stable' 2>/dev/null || echo "origin has no stable yet" + git fetch --quiet --tags origin + echo "## Weekly release (dry run from PR #${{ github.event.pull_request.number }})" >> "$GITHUB_STEP_SUMMARY" + bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run diff --git a/BROWSER.md b/BROWSER.md index 3047da8f5..886016ced 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -267,7 +267,7 @@ flow, and a layered prompt-injection defense — all behind a compiled CLI that prints plain text to stdout. ~100-200ms per call. Zero context-token overhead. It runs whenever the probe above does not print `READY`, and `$B` is a legitimate tool in that context; on a Mac with Aside open the skills never -reach for it. +reach for it. Its HTTPS trust has a known gap: see [TLS trust](#tls-trust-the-bundled-chromiums-root-store). --- @@ -275,7 +275,7 @@ reach for it. ```bash # One-time: build the binary (browse/dist/browse, ~58MB) -bun install && bun run build +bun install --production --frozen-lockfile && bun run build # Set $B once and forget about it B=./browse/dist/browse # or ~/.claude/skills/gstack/browse/dist/browse @@ -311,7 +311,7 @@ $B connect # headed Chromium + Side Panel extension 9. [Side Panel + sidebar agent](#side-panel--sidebar-agent) 10. [Pair-agent — remote agents over an ngrok tunnel](#pair-agent) 11. [Authentication + tokens](#authentication) -12. [Prompt-injection security stack (L1–L6)](#security-stack) +12. [Prompt-injection security stack (L1–L6)](#security-stack) — including [TLS trust: the bundled Chromium's root store](#tls-trust-the-bundled-chromiums-root-store) 13. [Screenshots, PDFs, visual inspection](#screenshots-pdfs-visual) 14. [Local HTML — `goto file://` vs `load-html`](#local-html) 15. [Batch endpoint](#batch-endpoint) @@ -562,7 +562,7 @@ from `snapshot`, or `@c` refs from `snapshot -C`. Full table: |---------|-------------| | `cookie =` | Set cookie on current page domain | | `cookie-import ` | Import cookies from JSON file | -| `cookie-import-browser [browser] [--domain d] [--profile p] [--all] [--clear-storage] [--verify-auth]` | Copy selected browser cookies; picker by default, explicit scoped or all-domain import, optional storage reset and sign-in assertion | +| `cookie-import-browser [browser] [--domain d] [--profile p] [--all] [--clear-storage] [--verify-auth]` | Copy selected browser cookies; picker by default, explicit scoped or all-domain import, optional storage reset and sign-in assertion. The imported session rides on the fallback browser's root store ([TLS trust](#tls-trust-the-bundled-chromiums-root-store)) | | `header :` | Set custom request header (sensitive values auto-redacted) | | `useragent ` | Set user agent (triggers context recreation, invalidates refs) | @@ -673,7 +673,7 @@ pass `--force-restart` to replace it explicitly (see "Daemon lifecycle" above). | Command | Description | |---------|-------------| -| `handoff [reason]` | Open visible Chrome at current page for user takeover (CAPTCHA, MFA, complex auth) | +| `handoff [reason]` | Open visible Chrome at current page for user takeover (CAPTCHA, MFA, complex auth). What you type there trusts the bundled Chromium's root store ([TLS trust](#tls-trust-the-bundled-chromiums-root-store)) | | `resume` | Re-snapshot after user takeover, return control to AI | ### Meta + chains @@ -970,9 +970,47 @@ first one set wins: 3. `HTTPS_PROXY`, then `https_proxy` 4. `HTTP_PROXY`, then `http_proxy` 5. `ALL_PROXY`, then `all_proxy` -6. no proxy (a direct connection) +6. no proxy configured by `$B` (a direct connection on Linux; the system proxy + settings on macOS and Windows) + +Whatever `$B` resolves is what Chromium gets, explicitly: a proxy is passed +as `--proxy-server=`, and a direct connection as +`--proxy-server=direct://`. On Linux, Chromium launched without a proxy +switch reads `HTTPS_PROXY`, `HTTP_PROXY` and `ALL_PROXY` itself, so every +direct connection there carries the switch: with `BROWSE_PROXY_URL=direct` +(or an unparseable variable, below) a set variable is ignored, and to use +one, name it through steps 1-5. Earlier releases left Chromium to read those +variables in the direct case (#3440), so a session that reported a direct +connection could still route through a proxy. Chromium on macOS and Windows +never reads those variables; it follows the system proxy settings (PAC and +WPAD included), and step 6 leaves it to them, so GStack Browser.app behind a +corporate proxy keeps working. `BROWSE_PROXY_URL=direct` bypasses the system +settings too, on every platform; on macOS and Windows it is part of the +daemon's settings, so a running daemon started without it is not reused (for +a headless daemon the refusal below names `BROWSE_PROXY_URL`; a `connect` +daemon gets the generic flags refusal) and a crash restart keeps it. The +daemon log (`browse-daemon.log` next to the state file) records the +effective mode each time the daemon starts, as `[proxy] using from +`, `[proxy] none: direct connection (--proxy-server=direct://; +ignoring HTTPS_PROXY)` (on macOS and Windows, `ignoring the system proxy +settings`), or `[proxy] none: system proxy settings` (an authenticated +`socks5://` upstream logs its `[proxy] upstream test ok` and `[proxy] bridge +listening on 127.0.0.1:` lines instead). A managed Chromium +proxy policy (`ProxyMode`/`ProxySettings`) still overrides the switch; `$B` +cannot see it, so on a managed machine the log line describes what `$B` +asked for, not what the policy enforces. Values from the environment follow the same credential policy as `--proxy`. +A variable without a scheme (curl-style `host:port` or `user:pass@host:port`) +is read as `http://host:port` (port 80 when none is given). For `host:port` +that is what Chromium's own environment reader took it as, so a value +Chromium on Linux used to pick up by itself now reaches the same proxy +through `$B`; `user:pass@host:port` follows curl's reading and is newly +proxied, because Chromium rejects any proxy spec with a userinfo. On macOS and +Windows such a value is now a configured proxy too, where it used to be +ignored with a warning in favour of the system proxy settings. `--proxy` and +`BROWSE_PROXY_URL` still require the scheme. + `socks5h://` is treated as `socks5://`, because Chromium's SOCKS5 already resolves names through the proxy. `NO_PROXY` (or `no_proxy`) is translated to Chromium's bypass list: `example.com` and `.example.com` cover the domain and @@ -982,10 +1020,19 @@ proxy source. When a server starts, `$B` prints one line naming the proxy and where it came from, with credentials redacted. - **`warning: HTTPS_PROXY is not a proxy URL browse can use (...)`.** The - variable doesn't parse as `scheme://host:port`: a curl-style `host:port`, an - unsupported scheme, or a bad port. `$B` connects directly instead of failing + variable doesn't parse even as `http://` plus the value: an unsupported + scheme (`ftp://`), a bad port, a missing host, a path other than a bare + `/`, a query or a fragment (where a `DOMAIN\user` login or a `/` in a + password ends up after URL parsing, so such values are refused for every + source rather than printed unredacted; percent-encode `/ ? #` in + credentials for every scheme, and `\` in `http://` and `https://` + credentials), or credentials that are not valid percent-encoding. A + credential-free value with a path (`http://proxy.corp:3128/pac`) is refused + the same way, where it used to be read with the path dropped. `$B` connects directly (on Linux, + Chromium is told so and does not read the variable itself) instead of failing every command. Fix: `export HTTPS_PROXY=http://host:port` (or `socks5://`), - or set `BROWSE_PROXY_URL=direct` to silence the warning. A bad `--proxy` or + or set `BROWSE_PROXY_URL=direct` to silence the warning (on macOS and + Windows that also makes Chromium bypass the system proxy settings). A bad `--proxy` or `BROWSE_PROXY_URL` is still an error, because those are explicit browse settings and a silent direct connection could expose your real IP address. - **`the running browse daemon was started with different proxy settings ( differs), so it was not reused.`** @@ -994,7 +1041,10 @@ where it came from, with credentials redacted. silently reuse a daemon that routes differently. The refusal prints the daemon's settings and this session's. Fix: run `browse stop` to restart the daemon with this session's settings (stop works whatever the proxy settings - are), or set `BROWSE_PROXY_URL=direct` when the running daemon has no proxy. + are), or match the daemon: unset the variable that differs, or on Linux set + `BROWSE_PROXY_URL=direct` when the running daemon has no proxy (on macOS and + Windows that opt-out is itself part of the daemon settings, so it only + matches a daemon started with it). **Stealth scope (Layer C, always on).** Every context — headless `launch`, `--headed`/`--proxy`, `handoff`, and the `useragent`/`viewport --scale` @@ -1296,6 +1346,39 @@ only writer when the chat-path agent was removed, so they reported stale or empty data. The live defenses report through their own call sites. See ARCHITECTURE.md § "Prompt injection defense" for the full threat model. +### TLS trust: the bundled Chromium's root store + +The fallback engine checks HTTPS certificates against the root store built into +its bundled Playwright Chromium. Its default headless browser launches with +Playwright's `--disable-component-update`, so it never receives the CRLSet +revocation updates Chrome pushes to running browsers. Aside uses your own +browser's trust store and updates. + +**Known gap: the compromised eMudhra intermediates +([#3338](https://github.com/garrytan/gstack/issues/3338)).** Chrome blocked +eight eMudhra intermediate CAs by CRLSet on 2026-10-17, the day their key +compromise was reported, and scheduled the removal of their root, emSign ECC +Root CA - G3, from the Chrome Root Store for 2026-10-27. The root store built +into gstack's pinned Playwright 1.62.1 (Chromium 151) still trusts that root, so +the headless browser accepts certificates those intermediates issued. That lasts +until gstack moves to the first Playwright release whose Chromium is +158.0.8115.0 or later, the first Chrome for Testing build measured to refuse +them. Until then, don't do signed-in work in this engine on a network you do not +trust, and prefer Aside for signed-in work where you have it. + +To check a build, run it on a network that does not inspect TLS, because a +captive portal or an intercepting proxy can fail some or all sites with the +same error. Run `$B stop`, confirm in the default headless mode that a +known-good HTTPS site such as `$B goto https://example.com/` loads, then run +`$B goto https://dv-g3b-emudhra.we-have-a-problem.com/`. Only +`ERR_CERT_AUTHORITY_INVALID` there means the root store no longer trusts them. +Any other error proves nothing. + +Caveats that depend on your environment (proxy variables, headed and +per-project profiles, `GSTACK_CHROMIUM_PATH` and `BROWSE_EXTENSIONS_DIR` +builds, and CRLSet timing in headed mode and `handoff`) are tracked in +[#3440](https://github.com/garrytan/gstack/issues/3440). + --- ## Screenshots, PDFs, visual @@ -1672,7 +1755,7 @@ browse/ │ ├── port-allocator.ts # Fixed 10000-49151 scan range for every long-lived listener (never port:0) │ ├── xprotect-heal.ts # macOS XProtect launch-kill classify + quarantine-clear + bounded reinstall │ ├── socks-bridge.ts # Local 127.0.0.1 SOCKS5 bridge that handles auth handshakes Chromium can't speak -│ ├── proxy-config.ts # --proxy URL parsing + cred resolution (URL vs env, fail-fast on both) +│ ├── proxy-config.ts # --proxy URL parsing + cred resolution (URL vs env, fail-fast on both); direct:// launch switch + proxy-mode log line │ ├── proxy-redact.ts # Cred-redaction helper for any proxy URL surfaced to logs/errors │ ├── xvfb.ts # Xvfb auto-spawn + orphan cleanup with PID + start-time validation │ ├── stealth.ts # Layer C: webdriver mask + window.chrome.* + Notification/Permissions + per-install hardware + toString proxy + automation-global sweep; buildGStackLaunchArgs (GSTACK_* cmdline switches); GSTACK_STEALTH=extended opt-in diff --git a/CHANGELOG.md b/CHANGELOG.md index 539b4e9d6..a65d6250e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [1.91.251.0] - 2026-10-26 **A free, deterministic design lint ships with gstack: `bin/gstack-design-lint` checks a page's HTML, CSS and components for the tells that make it read as machine-made, in under a second, and with `--render` measures what only a browser can.** Until now gstack's only mechanical design check was the opt-in impeccable engine most users never install, plus `gstack-design-craft`'s seventeen copy and ship-floor regexes; everything else in the Slop Gate was prose the model judged itself. The lint ports the roughly fifty-five machine-checkable gates of [Nutlope/hallmark](https://github.com/Nutlope/hallmark)'s `sloplint` (MIT; see NOTICE.md) into TypeScript under `lib/design-lint/`, rewritten in gstack's voice and mapped onto gstack's severity words. This release is the tool alone (Phase 1 of the hallmark absorption plan, approved by Garry on 2026-10-26); /design-html and /design-review start calling it in Phase 2, so no skill prose changes here. @@ -19,6 +19,329 @@ ### Gate integrity - No threshold, budget, timeout, trial count or assertion was weakened. No template, catalog or eval changed. Gate edits in the diff: `test/fixtures/module-size-ratchet.json` registers the 27 new `lib/design-lint` modules under the existing 800-line cap, and `test/helpers/free-fixtures-data.ts` maps `test/fixtures/design-lint/**` to the design-lint test files; `lib/aside-render.ts` gains the viewport on eval steps and http(s) `file`, described above. +## [1.91.250.0] - 2026-10-26 + +**Three free-tier fixtures stop racing themselves (GSTA-107 daily flaky pass).** Two tests polled for a result file's existence while its writer created it with a plain `writeFileSync`, so a poll between create and write read an empty file; a third copied a shared template repository with `fs.cpSync` and threw once inside the directory walk. + +### Fixed +- **`browse/test/dotenv-isolation.test.ts`** (Free Tests 39902683183 and 40322888041, shard 2, "JSON Parse error: Unexpected EOF" at the `agent-env.json` read): the probe writes its result to a temp file and renames it into place, and `waitForFile` waits for content, not only for the file. A new test feeds the old race (an empty file filled 150 ms later) and runs the probe source itself. +- **`test/claude-code-runner.test.ts`** (Free Tests 39694715116, shard 13, `running(pid)` false at the timeout case): the descendant publishes `descendant.pid` by rename, and the test polls `publishedPid()`, a positive integer, instead of the file's existence, so an empty read can no longer become pid 0. +- **`test/cso-git-hardening.test.ts`** (Free Tests 40288142343, shard 17, `fs.cpSync` threw in `fixture()` with an empty message and passed on the serial retry): every fixture builds its own repository with Git instead of copying a shared template, with auto-maintenance off (`maintenance.auto=false`, `gc.auto=0`) so the detached `git maintenance run` a commit starts no longer touches `.git` while a test walks or removes it; the `beforeAll` still pays the cold macOS Git spawns once, on a throwaway repository. No timeout changed. + +## [1.91.249.0] - 2026-10-26 + +**Five paid-case flakes from the 2026-10-26 train gates, repaired at their causes (GSTA-107 daily flaky pass).** The "ship-docsync family" was three distinct slips, not one shared cause: a mistyped `--help` path the detector refused although the usage had already been read, an undeclared refusal of gstack's own state directory as an artifact location, and a fixture that did not install the shared preamble a generated skill requires. tpa-present's consent question paraphrased "defer" as "wait for now", and qa-functional-webhook-fix ran the probe's `--help` bare before its first capture. + +### Fixed +- **ship-docsync-store (gate 40323989742 slice 3):** a candidate `--help` typed at a wrong, non-existent path (exit 127) is ignored when the declared binary's `--help` ran anywhere in the transcript, before or after, since a help probe reads nothing the agent still has to recover (`test/helpers/docsync-observer.ts` `candidateNotFound`); snapshot and compare still need the declared retry after the slip, and a wrong path that exists, a failed declared run, or no declared run at all still fail. +- **ship-docsync-completion (gate 40290033102 slice 2):** the parent put its snapshot `--out`, saved section and report insert under `/state`, gstack's own state directory (GSTACK_HOME), which the authority refuses but the fixture interface never named. The interface now says a private artifact lives directly under the fixture home or in a new directory there, and names every refused root: the repository, the installed skills, the Claude config dir, `/state` and `/remote.git` (`test/helpers/docsync-interface.ts`). The authority is unchanged. +- **docsync-spawned (PR #3434 screen 40269992077 slice 13):** `fixtureDocs` installs `/sections/preamble.md` beside the required sections when the generated skill has one, so a spawned child whose SKILL.md names the shared preamble as a required asset no longer blocks on its absence (`test/helpers/docsync-fixture.ts`). A generated skill without one installs exactly the files it did before. +- **tpa-present (gate 40290033102 slice 14):** the defer-option detector accepts "wait for now" and "hold off (on this) for now" as the option's own opening or sentence (`test/helpers/third-party-actions.ts` `offersDefer`); a drive option that waits while the user signs in, a "do not wait for now", or the phrase outside a lettered option still does not count. +- **qa-functional-webhook-fix (gate 40290033102 slice 9):** the fixture interface states that the evidence helper's `--help` is the only one it grants and that a bare `bun run probe` with `--help` or any argument outside a capture is a bypass (`test/helpers/qa-functional-eval.ts`); the grammar and the bypass detector are unchanged. + +### Tests +- `test/docsync-lifecycle-interface.test.ts` replays the captured store run (`test/fixtures/docsync-help-not-found-public.json`) as a pass, with controls for no declared `--help`, a failed one, and a wrong-path compare that only an earlier declared run precedes. +- `test/docsync-authority.test.ts` pins the interface sentence that names each refused root and replays the captured state-dir calls (`test/fixtures/docsync-state-dir-artifacts-public.json`): four refused commands and one refused write, all accepted once relocated to a new directory under the home. +- `test/ship-document-release-dispatch.test.ts` builds a generated root with and without a document-release preamble and checks the fixture installs it in the first case only. +- `test/third-party-actions.test.ts` adds the 40290033102 tpa-present render to the Opus consent captures (29 consent questions, 5 paraphrased defers) with new positive and negative wordings. +- `test/qa-functional-prompt.test.ts` pins the helper-only `--help` sentence before the observation forms and keeps `bun run probe -- --help` outside the grammar. + +## [1.91.248.0] - 2026-10-26 + +**`$B` in direct mode no longer lets Chromium pick a proxy from the environment (#3440 item 8).** `BROWSE_PROXY_URL=direct` (and an unparseable `HTTPS_PROXY`) only stopped gstack from configuring a proxy. The daemon inherits the whole environment and the launch passed no proxy switch, so Chromium read `HTTPS_PROXY`, `HTTP_PROXY` or `ALL_PROXY` itself and every `goto` still went through that proxy while `$B` reported a direct connection. That made the BROWSER.md precedence list false and any certificate check on a proxied machine unreliable. Reproduced on Linux with Playwright 1.62.1: a logging CONNECT proxy in `HTTPS_PROXY` received `CONNECT :443` with `BROWSE_PROXY_URL=direct` set. + +### Fixed +- **`browse/src/browser-manager.ts`**: each of `BrowserManager`'s four Chromium launch sites (headless `launch`, the `BROWSE_EXTENSIONS_DIR` persistent context, `launchHeaded`, `handoff`) adds `--proxy-server=direct://` when no proxy is configured and `forcesDirect()` says Chromium would otherwise pick one browse did not resolve, from one `proxyLaunchArgs()`. A configured proxy was already explicit (Playwright passes it as `--proxy-server`), so the environment can no longer win silently in either mode. `--no-proxy-server` was measured and rejected: chrome-headless-shell, the default headless build, ignores it and still took `HTTPS_PROXY`; the full Chromium honors both switches, and `direct://` works on both. The cookie-import worker (`cookie-import-native-worker.ts`), which launches the user's own browser only to read its cookie store and never navigates, is unchanged. +- **`browse/src/proxy-config.ts`**: `forcesDirect(env, platform)` is true on Linux always, where the leak lives, and on macOS and Windows only with `BROWSE_PROXY_URL=direct`. Chromium there never read the proxy variables and follows the system proxy settings (PAC and WPAD included), so a user of GStack Browser.app behind a corporate proxy keeps that connectivity, and a PAC cannot be named through `BROWSE_PROXY_URL`. An explicit `direct` bypasses the system settings on every platform. A standard proxy variable without a scheme (curl-style `host:port` or `user:pass@host:port`) is now read as an `http://` proxy (port 80 when none is given). For `host:port` that is what Chromium's own environment reader took it as: before this change browse rejected it with a warning and Chromium on Linux read the variable anyway, so that session was proxied; with the launch now explicit, reading it the same way keeps those sessions on their proxy instead of sending them direct. `user:pass@host:port` follows curl's reading and is newly proxied: Chromium rejects any proxy spec that carries a userinfo (`net/base/proxy_string_util.cc`), so that session was direct before and now sends its credentials to the named proxy. On macOS and Windows the same value is now a configured proxy too, where it used to be ignored in favour of the system proxy settings, so a daemon started under the old warning meets the mismatch refusal once. Only the plain `[user:pass@]host[:port]` shape is read that way; a value with a scheme (including the slash-less `https:user:pass@host:port`, which the URL parser reads as `https://`), a `/`, `\`, `?`, `#`, `@` or whitespace anywhere else in the value, or a bare number goes to the URL parser as written, so a bare number such as `1234` keeps its diagnostic and a one-slash spelling such as `http:/host:3128` is read as the URL parser always read it (`http://host:3128/`). A proxy URL with a path other than a bare `/`, a query or a fragment (a literal `?` or `#` anywhere) is now refused for every source (`--proxy` included), because that is where a `DOMAIN\user` login or a `/` in a password lands after URL parsing and only the userinfo is redacted; percent-encode `/`, `?` and `#` in credentials for every scheme and `\` in `http://`/`https://` credentials (a `socks5://` userinfo keeps a backslash as written). This narrows the environment path: a credential-free value with a path, such as `http://proxy.corp:3128/pac`, used to be read with the path dropped and the session proxied; it now warns and goes direct like any other unusable standard-variable value, and `--proxy`/`BROWSE_PROXY_URL` refuse it. Credentials that are not valid percent-encoding (a literal `%`) are a proxy configuration error instead of a crash, for every source. For a standard variable each of these parse problems is a warning and a direct connection, like any other unusable value; a scheme-less value with credentials beside `BROWSE_PROXY_USER`/`BROWSE_PROXY_PASS` is now refused like its `http://` spelling, which has always been a hard error (that value used to warn and go direct because it did not parse). A value that does not parse even as `http://` (an unsupported scheme, a bad port, a missing host) still warns and connects directly; `--proxy` and `BROWSE_PROXY_URL` still require the scheme. +- **`browse/src/cli.ts`, `browse/src/server.ts`**: on macOS and Windows the explicit `direct` is part of the daemon config hash (`explicitDirect()`), is recorded in the daemon state as `proxyDirect` and is re-emitted on a crash auto-restart from the session's own flags, so a running daemon started on the system proxy is refused rather than reused by a session that opted out (the refusal names `BROWSE_PROXY_URL` and offers `browse stop` or unsetting it; a daemon started before this release meets that refusal once, and a daemon with no recorded config hash is refused like one started without `--proxy`; the mismatch refusal offers `BROWSE_PROXY_URL=direct` as a remedy only where it would match the running daemon, on Linux or against a daemon started with it, and otherwise says to unset the variable that differs). The unparseable-variable warning's hint now says that `BROWSE_PROXY_URL=direct` also bypasses the system proxy settings on macOS and Windows. On Linux the opt-out hashes like no proxy, as before, because every direct connection there already carries the switch. +- **`browse/src/server.ts`** logs the effective proxy mode each time the daemon starts, beside the existing `[proxy] using from ` line: `[browse] [proxy] none: direct connection (--proxy-server=direct://; ignoring HTTPS_PROXY)` on Linux, naming each standard variable that was set and ignored; `(...; ignoring the system proxy settings)` for an explicit direct on macOS and Windows; and `[browse] [proxy] none: system proxy settings (Chromium's default on ; BROWSE_PROXY_URL=direct connects directly)` when those platforms are left to the system. +- **BROWSER.md, "Proxy from environment"**: says that whatever `$B` resolves is passed to Chromium explicitly, that on Linux a set proxy variable is ignored in direct mode, that macOS and Windows follow the system proxy settings unless `BROWSE_PROXY_URL=direct` is set (and that the opt-out is part of the daemon's settings there), where the daemon log records the effective mode, and that a managed Chromium proxy policy still overrides the switch. The eMudhra/TLS trust section is unchanged. + +### Tests +- `browse/test/proxy-direct-launch.test.ts` (Linux): two logging CONNECT proxies on loopback (`browse/test/helpers/connect-proxy.ts`) and a target name that does not resolve. With `HTTPS_PROXY` set and `BROWSE_PROXY_URL=direct`, the goto fails at DNS, no CONNECT reaches the proxy, the daemon log names the ignored variable and the state file has no proxy; this case fails on the unfixed tree with `ERR_TUNNEL_CONNECTION_FAILED` and a logged CONNECT. A scheme-less `HTTPS_PROXY` (`host:port`) is the configured proxy and the CONNECT reaches it (this case fails on the tree before the lenient parse); an `HTTPS_PROXY` that does not parse even as `http://` (`ftp://`) warns and still forces direct, so no CONNECT reaches the proxy it named. Two controls: `HTTPS_PROXY` alone is the configured proxy and receives the CONNECT; `--proxy` beside `HTTPS_PROXY` sends the CONNECT only to the configured proxy. Each case asserts on the daemon-log slice its own invocation wrote. +- `browse/test/proxy-direct-launch-paths.test.ts` (Linux, real Chromium under Xvfb): the `BROWSE_EXTENSIONS_DIR` persistent context, `launchHeaded()` and the `handoff()` relaunch each ignore an inherited `HTTPS_PROXY` (no CONNECT, `ERR_NAME_NOT_RESOLVED`), and a headed control with a configured proxy still sends the CONNECT to it. +- `browse/test/proxy-config.test.ts`: `forcesDirect` per platform, and `directProxyLogLine` for the Linux variable list, the explicit-direct line on macOS/Windows, and the system-proxy line. +- `browse/test/proxy-env.test.ts`, `browse/test/restart-env.test.ts`: the config hash separates an explicit direct from no proxy on macOS and Windows and not on Linux (`extractGlobalFlags` takes the platform); the mismatch refusal for both directions (explicit session against a system-proxy daemon, default session against an explicit-direct daemon) and the remedy an env-proxied session gets against a system-proxy daemon (unset the variable, not `BROWSE_PROXY_URL=direct`, which that daemon would refuse); `buildRestartEnv` re-emits `BROWSE_PROXY_URL=direct` from the invocation's flags only, never from the persisted state, and a proxied invocation wins over it; `resolveProxy` reads scheme-less standard variables as `http://` (with and without `user:pass@`, IPv6, no port, surrounding whitespace), leaves a one-slash scheme and a bare number to the URL parser, gives `h:3128` and `http://h:3128` the same config hash, warns for an unsupported scheme, a bad port, a missing host, a credential that would spill past the userinfo (`/`, `\`, `?`, `#`, including a `DOMAIN\user` login) or one that is not percent-encoded, without echoing credentials, refuses a proxy URL with a path other than a bare `/`, a query or a fragment (a bare trailing `?` or `#` included) for every source, keeps the protocol of a slash-less `https:user:pass@host:port` spelling, refuses mixed credential sources for the scheme-less spelling as for the `http://` one, and still fails closed for `--proxy` and `BROWSE_PROXY_URL`. +- `browse/test/proxy-env-daemon.test.ts`: through the real CLI and daemon, a scheme-less `HTTPS_PROXY` with credentials starts the daemon on that `http://` proxy, redacted in the start line and the state, and an `ftp://` value warns and falls back to direct; `BROWSE_PROXY_URL=direct` is written to the daemon state as `proxyDirect` where it bypasses the system proxy and a default session is then refused with the `BROWSE_PROXY_URL differs` lines; on Linux the daemon is reused. This file and `proxy-direct-launch-args.test.ts` join the macOS named-regressions CI job, so the refusal arm and the "no switch without an opt-out" arm run on macOS. `buildHeadedServerEnv` carries the opt-out to the headed server, and BROWSER.md's section is checked against the real switch and log literals. +- `browse/test/proxy-direct-launch-args.test.ts`: reads the real command line of the headless Chromium `BrowserManager` launched (from the pid it records) and asserts the switch is present on Linux and absent elsewhere without an opt-out, and present on every platform with `BROWSE_PROXY_URL=direct`; the per-platform expectations are written out, not read back from `forcesDirect`. Skipped on Windows, where no command line reader exists. + +## [1.91.247.0] - 2026-10-26 + +**/plan-eng-review no longer points its reader at three things its own text never defines.** The plan-eng-review sections judge marked down clarity and actionability for these references. All three live in shared resolvers, so the fixes reach every skill that renders them. The Claude Code hook that backs up AskUserQuestion carried the same stale pointer, so it's fixed too: + +### Fixed +- **Claims Need Evidence kernel** (`scripts/resolvers/preamble/generate-evidence-directive.ts`): the evidence-kind bullet ended with "Reuse rules: Step 16." That step exists only in /ship, so in every other skill the pointer led nowhere. It's gone. /ship's Step 16 still states its own reuse rule ("Reuse a check when its inputs match"), and every tier's DONE line still asks you to name reuse. +- **AskUserQuestion Tool resolution** (`generate-ask-user-format.ts`): "per its Unattended session block" read like a section of the skill, but the block is runtime output. The change removes the dangling pointer from item 1 and from the failure fallback's `unattended` branch. Both keep the destructive/irreversible carve-out the pointer used to supply: item 1 now names destructive or irreversible options among the pending gate items itself, and the fallback branch says a destructive or irreversible option stays pending, as the printed `unattended-session` block does. +- **AskUserQuestion fallback hook** (`hosts/claude/hooks/auq-error-fallback-hook.ts`): its unattended directive ended with "(Unattended session block; docs/unattended.md names each skill's record)", the same section-style pointer to runtime output. It now ends with "(docs/unattended.md names each skill's record)". The rest of the directive, including which skills print a GSTACK_RESULT line, is unchanged. +- **Reality rows, eng/ceo branch** (`scripts/resolvers/plan-reality.ts`): the Completion Summary row and plan-eng-review's checklist ask for the `PLAN_REALITY` line, but the operative block never said where that line comes from. It now says `gstack-plan-reality check` prints it. + +### Tests +- `test/honesty-kernel.test.ts` no longer requires the Step 16 pointer. It now rejects any `Step ` cross-reference in the kernel, which every skill renders. +- `test/resolver-ask-user-format.test.ts` checks that item 1 states the unattended pending set with no section or block pointer, in any spelling of the unattended-session block or "per its printed". It also checks that the failure fallback's `unattended` branch keeps destructive or irreversible options pending, and that the hook's unattended directive says the same. `test/gstack-skill-start.test.ts` checks that the block gstack-skill-start actually prints keeps destructive or irreversible options pending for every skill it samples. `test/auq-error-fallback-hook.test.ts` asserts the hook's unattended directive no longer says "Unattended session block". +- `test/plan-reality.test.ts` checks that the reality-rows block for the eng and ceo phases says its `gstack-plan-reality check` line prints `PLAN_REALITY`. +- 89 pins in `test/fixtures/skill-md-size-allowlist.json` were moved to their new exact sizes, and every pin went down. 78 went down by 32 characters. The 11 non-Claude plan-eng-review pins went down by 8, because they also inline the 24-character "prints `PLAN_REALITY`" pointer from `review-sections.md`. Both figures already include the destructive/irreversible carve-out that item 1 and the fallback branch keep. The /ship goldens were regenerated. + +### Gate integrity +- No threshold, rubric, trial count, case kind or judge prompt changed. Every size pin went down (78 by 32, 11 by 8); none was raised. + +## [1.91.246.0] - 2026-10-26 + +**The README is a first-ten-minutes page again: what gstack is, how to install it, the first three skills to try, the skills tables, platform notes, and links to docs/ for everything else.** It was 862 lines and 90 KB, with CSO producer sandboxing, Codex model-selection order and the uninstall script in the path of someone deciding whether to try it. It is now about 300 lines and 33 KB. Nothing was deleted: every removed section moved to a docs/ page, verbatim except the "Proactive skill suggestions" paragraph in docs/why-gstack.md, which now describes the core-set default and `gstack-config set proactive core|all|off` landing in #3432, and every anchor other files link to (`#install--about-30-seconds`, `#gbrain--persistent-knowledge-for-your-coding-agent`) still resolves. + +### Changed +- **`README.md`**: a short intro, a three-skill quick start (/office-hours, /review, /qa), the two-step install with the host matrix, the demo transcript, the sprint table, the review picker and the power-tools table unchanged, a short GBrain section, new "Platform notes" (macOS, Linux, the Windows paragraphs), the Docs table with the new pages, a one-paragraph privacy summary and a short troubleshooting section that keeps the CLAUDE.md `/browse` rule. + +### Added +- **`docs/why-gstack.md`**: the Karpathy quote, the LOC numbers and their caveats, the contribution graphs, "Karpathy's four failure modes", "Parallel sprints" and "10-15 parallel sprints". +- **`docs/install.md`**: setup timing, the requirements paragraph in full, the CSO image-preload window, team mode's Node.js hook, and the hooks `./setup` registers (the timeline Stop hook and canonical-only hook paths). +- **`docs/uninstall.md`**: the uninstall script and the manual removal steps, CLAUDE.md cleanup, Playwright and Aside notes. +- **`docs/hosts.md`**: OpenClaw and the ClawHub skills, which agents a checkout renders for, the Copilot and Antigravity notes, outside-review providers, the `/claude` rename, the instruction-only digest, the Codex behavioral profile and model selection order, the plan-review and implementation tiers, the design binary's models, and adding a host. +- **`docs/qa-without-a-webpage.md`**, **`docs/standalone-binaries.md`** (every `gstack-*` CLI plus the CSO evaluation producer contract and its host matrix), **`docs/voice-input.md`**, **`docs/privacy.md`** (the full Privacy & Telemetry section). +- **`docs/domain-skills.md`** gains the README's summary of domain skills and the raw CDP escape hatch; **`docs/gbrain-quick-tour.md`** holds the README's short version of the four paths, trust policy and memory sync; **`docs/troubleshooting.md`** gains a "Quick answers" section with the README's short troubleshooting entries. + +## [1.91.245.0] - 2026-10-26 + +**`gstack-ship-queue ready` counts CI the way GitHub's rollup does, and names the way through a CHANGELOG-only conflict.** A superseded run (gbrain saw a cancelled `contributor-gate` beside three later passes on the same head) made `ready` print `failed=1` where GitHub showed 0 failed, and a PR that conflicts with main on CHANGELOG alone had no documented route to green CI on its head. + +### Fixed +- **`ready`'s `ci:` line** (`lib/ship-queue.ts`, new `latestRunPerName`) judges each check (one app's check name) by its latest run (newest `completed_at`, then highest id; a run still in progress is newest), so a cancelled or re-run earlier attempt no longer counts as a failure; two apps reporting the same name stay separate, a run without a name or without a readable `completed_at` counts on its own, and commit-status contexts are counted one each, as before. +- **`ready` prints a `ci-hint:` line on a conflicting PR** (`gh pr view` now also reads `mergeable`, `headRefName` and `isCrossRepository`): with no CI at the head it names the procedure (dispatch `free-tests.yml` and `windows-free-tests.yml` with `gh workflow run … --ref `, clear the pregate lanes with the run URLs, rerun `ready`); with green runs at the head it says the dispatched runs count; a head on a fork gets a hint that leaves the dispatch to the coordinator; a branch name outside `[\w./-]`, or starting with `-`, prints as ``. Every hint ends in `(advisory)`; nothing parses it. No hint without a conflict, or while the runs are red or pending. +- **Docs:** `docs/ship-policy.md` records that procedure under "Lane: report ready" (and that `--screen` is `QUEUE_SCREEN_STALE_REF` on such a PR for want of a merge ref, with advice that does not apply to a CHANGELOG-only conflict), rewords the `stamp-at-merge` row's `next:` description to the train wording `gstack-restamp` prints since #3439 (`commit and push only the stamp diff above (FILES), with no edit of your own`), and rewords the restamp `changelog` stage so `CHANGELOG_DUPLICATE_HEADING` is described as #3439 implements it: only a duplicate the PR introduces counts, measured against the base's CHANGELOG. + +### Tests +- `test/gstack-ship-queue.test.ts`: two runs of one check name where the older is cancelled read green (either order), a newer failure reads red, a newer in-progress or queued run reads pending, equal `completed_at` falls back to the id, unnamed and undated runs stay separate, two apps with one name stay separate, a null conclusion reads red, and a commit status never collapses with a check run of the same name; a conflicting PR with no CI refuses with the dispatch hint, with green dispatched runs READYs with the "counts" hint, a fork head and an odd or dash-led branch name get the safe hint, and no hint appears without a conflict or while runs are red or pending. + +## [1.91.244.0] - 2026-10-26 + +**A restamp PR no longer fails pregate for the manifests it stamps.** gbrain's `bun run release:restamp` rewrites its plugin manifests and VERSION; its base's `.gstack/pregate.json` declares no lane for them, and pregate reads policy from the base, so every such PR failed `lanes` with `PREGATE_NO_LANE` and could not fix itself (gbrain #7625, #7606). + +### Fixed +- **`gstack-pregate` lanes**: a touched `*.json`, `*.plist`, `VERSION` or `VERSION.txt` file whose diff against the merge base changes nothing but its version values is waived as version-stamp-only, on its own line (`version-stamp-only (no lane needed): — only version values changed against the merge base; a manifest whose other content changed still needs a lane`), with the per-file verdict under `--explain`. The rule is exact per kind (`lib/pregate/stamp-only.ts`): JSON objects of the same shape (same own keys in the same order, same array lengths, same leaf types) whose only changed leaf is the document's top-level `version`, a bare version on both sides, with the text differing on that line alone; plist `` versions under `CFBundleShortVersionString` or `CFBundleVersion`; a VERSION file that is one bare version on both sides. Nothing nested qualifies (a dependency pin, a lockfile entry, a catalog), texts are compared byte for byte outside the stamped value (a line-ending change or a BOM is content), and a changed description, a new, removed or reordered key, a floor such as `minHostVersion` or `MinimumOSVersion`, a change the parser would erase, an invalid-UTF-8 or over-1-MiB file, a mode change or symlink, an added, deleted or renamed manifest, a file under a test, fixture, golden, `.gstack/` or workflow tree, or any other file kind is not a stamp and keeps its `PREGATE_NO_LANE`. It applies only after the import graph, the declarations, the regen registry and both empty lanes found nothing, so a manifest a test reads still selects that test. `docs/pregate.md` and the `PREGATE_NO_LANE` troubleshooting entry state the rule. + +## [1.91.243.0] - 2026-10-26 + +**The launch environment reaches every gstack command and child through a sourceable file, and nothing in a skill fence may print the environment (GSTA-104).** During GSTA-96's unattended /ship on #3425 (2026-10-26), the Step 14.5 docs-audit child was told to export the `GSTACK_LAUNCH` assignments and re-typed them as `export $(...)`. The substitution came back empty, the shell ran a bare `export`, and the values of ANTHROPIC_API_KEY, OPENAI_API_KEY and UBICLOUD_API_KEY landed in its tool output. The line itself carries no secret (PATH, GSTACK_SESSION_KIND, GSTACK_STATE_ROOT, GSTACK_EPHEMERAL, GSTACK_CAPY, GSTACK_INSTALL_VERDICT); the hazard was re-typing it. Now the installer writes the assignments as a file callers source, every fence that hands the launch environment to a child says so in one line, a free ratchet keeps environment dumps out of every skill fence and shell helper, and the redaction engine refuses a credential value from the writer's own environment, whatever its shape. + +### Added +- **`GSTACK_LAUNCH_FILE: .env`** after the `GSTACK_LAUNCH` and `GSTACK_LAUNCH_PATH` lines of every passing `bin/gstack-capy-install` run, `--check` included . The file is part of the ok verdict: a passing run that cannot write it exits 1 with `gstack: fail launch-file — could not write the launch file ; fix: … (LAUNCH_FILE_UNWRITABLE)`, and a failing run announces no launch file and removes the one an earlier passing run left beside the verdict it rewrites (a profile check that keeps a fuller ok verdict leaves that verdict's file; an unwritable directory keeps both the old file and the old verdict), so no caller is left with only the line and the re-typing path. The file holds the same assignments as `export` statements (the PATH prepend is idempotent), is written atomically with mode 0600 beside the install verdict under the state root, and is applied with `. ` at the start of the shell that runs a gstack command. A caller hands a child the path, never the values. +- **`env.value` (HIGH) in `lib/redact-engine.ts`**: the value of every `*_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, `*_PASSWD`, `*_PASS`, `*_PAT`, `*_APIKEY` or `*_PWD` variable in the scanning process's environment (any case; eight characters or more after trimming, not a placeholder; names carrying a `PUBLIC` word and gstack's own `GSTACK_BRAIN_LOCK_TOKEN` are exempt, and there is no path exemption: a path-valued `SSL_KEY` that must appear in a text goes through `gstack-redact --allowlist` or the pre-push hook's `.gstack-redact-allowlist`, and for the writers that take no allowlist the variable is set empty for that one command) is a finding wherever it appears in the scanned text, so a leaked value cannot reach a PR body (`gstack-post`), a ship receipt, a report, an approval row or a Drive mirror through any writer that scans with the engine. The value is matched in the same normalized form as the text, so a value NFKC rewrites still finds its leaked bytes; when one value is a prefix of another, the longer value is redacted whole; the finding's preview never shows a byte of the value. `redactFindingSpans` removes it on the machine-egress path; `ScanOptions.processEnv` names the environment (`{}` scans without one). `test/redact-env-value.test.ts`. +- **`test/launch-env-echo-ratchet.test.ts`**: every generated SKILL.md and section file, their templates, `autoplan/DRIVER.md`, the Capy skill, every shell helper under `bin/`, `setup` and the `scripts/*.sh` install scripts carry no `export $(...)`, `export \`...\`` or unquoted `export $VAR`, no `env` or `printenv` (flags alone included), and no bare `set`, `export`, `declare -x`, `typeset -x`, `declare -p` or `typeset -p` in any command segment of a fence (`set | grep '^NAME_'` stays allowed: the anchored filter bounds what prints). `test/fixtures/launch-env-echo/` holds the negative controls the scanner must trip on. + +### Changed +- **`ship/sections/documentation.md.tmpl`** (Step 14.5 subagent prompt), **`document-release/SKILL.md.tmpl`** (spawned note), **`autoplan/DRIVER.md.tmpl`** and the runtime `capy-asks` block of **`bin/gstack-skill-start`** (every workflow started with `GSTACK_CAPY=1`, /review and /qa included) each say in one line: apply the launch environment with `. `; never `export $(...)`, `env`, `printenv` or a bare `set` in a tool fence, because a bare `export` prints every variable, credentials included. /review and /qa dispatch no child that runs a gstack command, so their SKILL.md files, whose sizes are pinned, are unchanged. Nearby prose in the Step 14.5 section was tightened so /ship's union stays under its parity cap (313,279 of 313,281 bytes). +- **`capy/skill/gstack/SKILL.md`** (the source of the Drive skill) and **`docs/capy-install.md`** describe the launch file and name the incident; the Drive copy needs the owner's recopy to say the same. +- **`scripts/lib/shard-engine.ts`** `createShardSandbox` and the test preload (`test-setup.ts`, so a direct `bun test ` matches the lanes) drop `GSTACK_SESSION_KIND`, `GSTACK_CAPY`, `GSTACK_EPHEMERAL` and `GSTACK_INSTALL_VERDICT` from every test's environment, so a run started from a shell that sourced the launch file no longer hands the product-under-test children an unattended Capy session they never set up (GSTA-99's finding); each fixture sets its own session kind. `GSTACK_STATE_ROOT` and PATH are kept. + +### Gate integrity +- One test budget changed: `test/setup-gbrain-remote-caller.test.ts`'s late-completion case gives its second attempt 2s (was 250ms) with a 20s test timeout (was 10s) after it failed 4 of 4 on windows-latest once the new files repacked the Windows shards (run 40263984171). No other gate, threshold, budget, timeout, trial count or case kind changed. The redaction scan only gained findings, minus the framework-public and non-secret exemptions named above. Size pins are unchanged: the edited skills are under the limit. + +## [1.91.242.0] - 2026-10-26 + +**`gstack-context-bill` now bills a real install.** On `~/.claude/skills` it reported "2 skills, ~134 tok" always-on, because setup installs each skill as a real directory whose `SKILL.md` and `sections/` are symlinks into the gstack checkout, and the walker only counted regular files. The tool built to answer the token-cost complaint gave a falsely tiny answer on the layout users actually have. On a fresh install it now reports 58 skills, ~6.8K tokens always-on, and 3.6 MB on disk (it was 37 KB across 2 skills). Billing the checkout itself is unchanged (66 skills, ~7.2K tokens). + +### Fixed +- **`lib/context-bill.ts`**: `findSkillDirs`, `walkMd` and the foreign-file check follow symlinked files and directories. Every path the bill reports, and every file `--exact` keys on, stays the lexical path under the root you passed. A link to a directory that contains the tree being billed or the skill being walked (`..`, `/`, `$HOME`) is never followed, so a link cannot climb out of the root into its parent or your home directory, even from a symlinked skill that lives elsewhere. The markdown walk expands each real directory once and counts each real file once per skill, under the first path reached in sorted order, which is how a skill loads it: loops end, links that fan out into each other cannot multiply the walk, and a tree with more than 200,000 entries is refused with an error instead of being cut short. `--exact` walks and sends only what the bill walks (the same per-skill walks and fences, so a link no skill bills is never walked), each real file once, measures frontmatter only for the `SKILL.md` files the bill prices it for, and prices every link to it with that one measurement. It sends only files inside the tree, inside the gstack checkout that the tree's `gstack/` entry links to (a git checkout with `VERSION`, `setup` and a `SKILL.md` named `gstack`; `bin/dev-setup`'s `.claude/skills/gstack -> ` counts), or inside gstack's render folder `$GSTACK_STATE_ROOT/render` (where setup renders the `SKILL.md` files that gbrain, `--claude-model` and per-install renders link to; trusted only when `render` is a real directory, not a link). Each file is judged by its realpath, so a link out of one of those folders is refused too; anything else a symlink reaches (a skill's `notes -> ~/Documents`, say) is not sent, its directory is named in the notice, the files are listed, and they are estimated offline. The notice also says how many sent files come from the checkout and how many from the render folder. A bill in which `--exact` had to estimate any item (a refused file, or one that changed after it was measured) says so: `tokenSource` starts with `mixed:` and names the count, `tokenEstimateErrorPct` keeps the estimate's 40% band, and the text output says the counts are mixed instead of measured. A measurement is reused only for a file that still has the realpath, inode, size and mtime it was measured with, so a link retargeted or a file rewritten after the measurement (during `--diff`, say) is estimated and counted as estimated instead of inheriting the old count. Billed paths are matched to their measurement one link at a time, so a second skill sharing a link chain deeper than the kernel's limit is still priced from the one measurement. Files are opened without blocking, so a file swapped for a FIFO is refused instead of hanging the run. An empty `.md` file and a `SKILL.md` without frontmatter are measured as zero tokens without a request, and a whitespace-only `.md` file is not sent and is estimated (so the bill says mixed), because `count_tokens` rejects content with no non-whitespace text: a blank file used to drop every measurement with a 400, and an empty one turned a fully measured bill into a mixed one. When every file `--exact` could send is blank, nothing is sent, no receipt is written, and the notice says so (and whether other files were refused). The sending notice is printed after the files are read, just before the receipt, and counts exactly the files sent. A state root that is missing, unreadable or not a directory just means there is no render folder to trust; the bill still runs. Each file is read through the realpath its walk recorded and only while it is still that same file, so a link retargeted or a file swapped for a link mid-run is refused rather than sent. A link named `*.md` counts only when its target is a `.md` file, so `--exact` never sends a non-markdown file through a markdown-named link. `walkMd` skips a child repo checkout (the `gstack/` link beside the installed skills), as `findSkillDirs` already did, so `--exact` does not send the whole checkout. The walk and skill discovery read each directory through its realpath, so a chain of links deeper than the kernel's 40-link limit no longer drops files or skills silently. Skill discovery has the same 200,000-entry cap as the walk. A container skill skips only nested directories whose `SKILL.md` is a file, as discovery does, so a `sub/` whose `SKILL.md` is a directory is billed to the container instead of to nobody. The root container also skips a nested skill that a directory link reaches at any depth (discovery already bills it), so `refs/alias -> ../sub` no longer counts `sub`'s files a third time in TOTAL. `--exact` on a path that does not exist now stops with `No such tree` before any request, including `--diff` with one missing tree (it used to send its one-character envelope probe first, and `--diff` sent the whole first tree), and its notice counts the same list of files it then sends. An existing tree with no skills sends nothing and writes no egress receipt, a tree whose skills have no markdown file to send says the offline estimate was used, and a tree whose every billed file was refused says so. `--help` now says which `.md` files `--exact` sends and that others are named and estimated. + +### Tests +- `test/context-bill.test.ts`: an installed-layout fixture (a checkout outside the skills root, real skill directories with symlinked `SKILL.md` and `sections/`, and a `gstack/` link to the checkout) bills both skills with the target's exact frontmatter, `SKILL.md` and section bytes, and keeps every walked path under the root. Negative controls: a dangling `SKILL.md` link and a `SKILL.md` link to a directory are not skills. A second fixture holds a self link, a parent link that reaches a skill outside the root, a loop back into the skill and a link up to the temp root; only the one real skill and its own files are billed. Further cases cover a loop to a directory below the walk root, a root whose name starts with `..`, a symlinked `agents.md` (with a dangling link and a directory of a foreign-file name as controls), `--exact` on the installed layout (every billed file measured; nothing behind `gstack/` or a `.md`-named link to a non-markdown file is sent), a 12-level fan-out of links that reaches one file by 2^12 paths (walked once, where a walker that expands every link would return all 4,096), two skills linking one checkout file and directory (each real file sent once, both skills priced from it), a symlinked skill outside the root with a link up to the root's parent, the `--exact` notice's count of files from the gstack checkout, a skill whose `SKILL.md` another skill's walk reaches first under another name (its frontmatter still measured), a container skill whose nested skill links to the container's own files (both skills still bill them), `--exact` on a missing tree and `--diff --exact` with a missing second tree (named, with no request sent), a symlinked skill with a link up to its real parent (the files there are not billed, so not sent, and no billed file is keyed through that link), an existing tree with no skills (no request, no receipt), a skill whose `SKILL.md` links to a non-markdown file (no request, no receipt, and a line saying the estimate was used), a reference file with frontmatter (no frontmatter request for it), an install whose skill also links `notes` to a documents folder (the checkout files are sent; the documents file is refused, named, listed and estimated), a `gstack/` entry that is not a gstack checkout (a plain folder, a bare `.git` folder, another project's repo, or a gstack-shaped folder without `.git`: nothing behind it counts as the checkout), a `bin/dev-setup` tree inside the gstack repo (its skill files are sent), refused files in nested and sibling folders (each folder named once), every billed file refused (said so), a link retargeted, a file replaced by a link or a hard link, or a parent folder swapped during `--diff --exact` (never sent; a swapped file stops the run), a 50-deep chain of links (every file walked, billed and, under `--exact`, measured), a skill 45 directory links deep (discovered), skill discovery past its entry cap (refused), a `sub/` whose `SKILL.md` is a directory (billed to the container), a tree with no skills under `--exact` (no estimate line), a root container with a `refs/alias -> ../sub` link and a link to an outside folder holding a skill (neither nested skill billed to the container), a file swapped for a FIFO after the walk (refused within the subprocess timeout, where it used to hang), two skills sharing a 50-deep link chain (both priced from one measurement, nothing estimated), a `SKILL.md` link retargeted to a bigger file and a reference rewritten in place after measurement (both estimated and counted as such), a bill with a refused skill (`mixed:` in `--json` and in text), a file replaced by one of the same size and mtime (estimated), a skill without frontmatter plus an empty reference under a `count_tokens` stand-in that rejects empty content (still fully measured), a render folder that is itself a link (not trusted), a tree whose only file is empty, alone and beside a refused link (no request, no receipt, and a notice naming which case it is), a whitespace-only reference under a stand-in that rejects it (not sent, estimated, the bill labelled mixed, and the notice counting only the file sent), the render folder found through `GSTACK_STATE_ROOT` with no state root passed in, a state root that is a file (the bill still runs, measured), a git checkout inside a skill or linked from one (not billed; this case passes on main and fails when the walk's `.git` check is removed), and a render install (its `SKILL.md` and sections sent from `$GSTACK_STATE_ROOT/render`, a link from there to a documents file refused and listed). The installed-layout and escape fixtures fail on main (the first finds no skills, the second bills the outside skill), the missing-tree case fails on main (one request sent), and each later case failed on the commit before its fix, except the hard-link and parent-swap cases, which pin the identity check added with the realpath reads (they fail when that check is removed). + +### Gate integrity +- No threshold, budget, timeout, trial count, case kind or assertion changed. The bill of this repo's own tree, which the context-budget ratchet reads, has the same skills, bytes and rounded token figures as on main. Four raw `totalMdTokens` values in `--json` differ in the last floating-point digit, because each skill's files are now summed in sorted order. + +## [1.91.241.0] - 2026-10-26 + +**The weekly OSV scan on main is green again, so the weekly release cut can run.** The 2026-10-26 scan ([run 40288353843](https://github.com/garrytan/gstack/actions/runs/40288353843), #3462) found 14 fixable vulnerabilities in six packages across both lockfiles, and the release workflow skips any main commit with a red check run. Every one is bumped to its fixed release; no application code changed, because gstack imports none of these packages directly (the MCP SDK and zod arrive through `@anthropic-ai/claude-agent-sdk`, body-parser through its express, fast-uri through ajv, ip-address through socks and express-rate-limit, dompurify through mermaid). + +### Fixed +- **dompurify 3.4.16 → 3.4.17 in `lib/diagram-render`** (GHSA-cg2g-xh64-v4q7, CVSS 9.2; GHSA-mrv2-3pqr-j5jr; GHSA-c6gr-qvv4-5v3f). The override moves and the committed bundle is rebuilt with Bun 1.4.2 (two builds, one sha256 `c2d37f9f…`), so the shipped page runs the fixed sanitizer instead of a bundled copy of the old one. Mermaid, excalidraw and React pins are unchanged. +- **@modelcontextprotocol/sdk 1.31.0 → 1.32.0** (GHSA-22jm-h49p-29qw, GHSA-46jr-4wx8-v453, GHSA-rvq5-wwqv-78pq, GHSA-6prh-2h8m-c8cw), **fast-uri 3.1.8 → 3.1.9** (GHSA-w74x-v6f9-j3xg, GHSA-wjr9-j795-jxcr, GHSA-333x-w4r6-6crp, GHSA-xhq7-xmqj-769h) and **ip-address 10.7.3 → 10.7.4** (GHSA-rxp7-6hxx-x8q5) through the existing `overrides` pins. +- **zod 4.5.4 → 4.6.7** (GHSA-mrhx-c7xc-m3w5, fixed in 4.6.6) and **body-parser 2.3.0 → 2.4.0** (GHSA-27r3-jxmh-p828) through the lockfile alone; both sit inside their consumers' ranges, so no override was added. body-parser 2.4.0 brings a nested qs 6.16.1. + +### Changed +- **`lib/diagram-render/scripts/build.ts`** empties the one `VITE_APP_FIREBASE_CONFIG` literal that `@excalidraw/excalidraw`'s npm bundle carries (excalidraw.com's public Firebase client key, which nothing in the package reads and the offline render page never uses). The build fails if the literal is not matched exactly once or any `AIza…` key survives, so an excalidraw bump that changes its shape cannot put the key back silently. Without this, every rebuild of the committed bundle failed the pre-gate secret scan. + +### Tests +- `test/diagram-render-drift.test.ts` asserts the bundle's DOMPurify is the `package.json` override and no other version, and that it carries no Google API key; its path constants are repo-relative segment runs so the pre-gate's import graph selects it for the bundle, `BUILD_INFO.json` and the build script. +- `test/deps-smoke.test.ts` refuses `ip-address` below 10.7.4 and checks every resolved version of the six packages against its OSV-fixed floor in both lockfiles (all six fail on main's lockfiles). OSV-Scanner 2.3.8 with the workflow's own arguments (`--config=.osv-scanner.toml --include-git-root --recursive ./`) reports no issues on this tree; the katex ignore is the only filtered advisory. + +## [1.91.240.0] - 2026-10-26 + +**Proactive skill suggestions default to the four core skills.** `proactive` now takes `core` (the default: only /office-hours, /review, /qa and /ship auto-invoke when a request matches), `all` (any matching skill, the old behavior) or `off`. `true` and `false` keep working as `all` and `off`. The sentiment research (GSTA-93) found auto-invocation of every skill is what makes gstack collide with other skill packs; the four core skills are where the praise is, so they keep the nudge. Garry, 2026-10-25: "do it". + +### Changed +- **`bin/gstack-config`**: `proactive` defaults to `core`; `set proactive` accepts `core`, `all`, `off`, `true` and `false` in any case, stores the YAML yes/no family (`yes`/`on`/`1`, `no`/`none`/`0`/`disabled`/`never`) as `all`/`off`, and warns on anything else, storing `core`. The config header documents the three values. +- **`SKILL.md.tmpl`** (the router): three branches, `off`, `core` and `all`; under `core` only the four core routing rules auto-invoke, the browser step and the Failures branches name `/browse` or `/investigate` for the user to type, and a skill the user names is always invoked. The opt-out line writes `off`, and `all` and `core` have their own lines. +- **Every skill's preamble** (`scripts/resolvers/preamble/generate-upgrade-check.ts`): the steady-state rule names the four core skills under `core`, scopes the setting to unasked suggestions, carves out only the skills the user names or the running skill invokes itself ("Skills the user names or this one invokes run", so `/ship` dispatching `/document-release` is not a suggestion, while an in-skill offer to run another skill still counts as one under `off`), reads only `core`, `all` and `off` because `gstack-skill-start` now reports the legacy spellings as those, and is exactly as long as before, so the 89 size pins in `test/fixtures/skill-md-size-allowlist.json` are unchanged. +- **`bin/gstack-skill-start`**: the one-time prompt offers "Keep the core four (recommended)", "Suggest every matching skill" and "Turn it off", writing `core`, `all` or `off`. `scripts/question-registry.ts` lists the same three options. +- **`bin/gstack-skill-start`**: the `PROACTIVE:` STATUS fallback when `gstack-config` is missing is `core`, not `true` (found by /ship's coverage audit, red at the first head); a stored value gstack does not know reads as `core`, the legacy `true`/`false` and the yes/no family read as `all`/`off`, and a hand-edited value is unquoted, lowercased and stripped of a trailing comment first, so STATUS only ever says `core`, `all` or `off` and a typed opt-out never fails open (found by /ship's red team: `proactive: "off"` read as `core`); the first-run and first-loop tips name only core skills and stay quiet under `off`; the one-time offer to add `## Skill routing` to the project's CLAUDE.md is not made under `off` (found by /ship's red team: the routing rules are the auto-invocation the user turned off); a user who stored `off` before the one-time proactive prompt is not asked (the prompt marker is written before STATUS reports `PROACTIVE_PROMPTED`). +- **`office-hours/sections/design-and-handoff.md.tmpl`**: the next-review handoff takes the recommend-only path under `off` as well as the legacy `false` (found by /ship's review); under `core` it still offers the review, because a step inside an invoked skill is part of that skill, which the router now says in so many words. + +### Added +- **`docs/proactive.md`**: the three values, who each is for, the legacy spellings, the commands, and the approved fallback (`off` if collisions continue). Linked from docs/skills.md. + +### Tests +- `test/proactive-config.test.ts`: the default in get, list and defaults; the five accepted values stored verbatim, an upper-case `OFF` stored as `off`, the yes/no family stored as `all`/`off`; the warning and `core` fallback on an unknown value (negative control: `maybe`). +- `test/proactive-core-set.test.ts`: every surface (config header and default, router template and render, preamble, one-time prompt and registry, docs, office-hours gate, tips) names the same four core skills and the same three values, with negative controls (no non-core skill in the core block, `true` no longer "the default", no `set proactive false` left). +- `test/proactive-skill-start.test.ts` runs the script: STATUS is `core` when unset or unknown, the stored value otherwise, `all`/`off` for the legacy spellings and for a quoted, upper-case or commented hand edit; the missing-`gstack-config` fallback equals the config default (regression test: failed at the first head); the CLAUDE.md routing offer and the first-run and first-loop tips fire under `core` and not under `off` (marker unwritten). +- Known gap, follow-up #3446: under `core`, the `## Skill routing` section gstack offers for a project's CLAUDE.md, and the "Proactively suggest" sentences in non-core skill descriptions (full catalog mode), still route every skill; docs/proactive.md says so. + +### Gate integrity +- No threshold, budget, timeout, trial count or case kind changed. Size pins unchanged (the preamble is byte-for-byte the same length). + +## [1.91.239.0] - 2026-10-26 + +**BROWSER.md now says that gstack's own browser still trusts the compromised eMudhra intermediates, and how to check (#3338).** Chrome blocked eight eMudhra intermediate CAs by CRLSet on 2026-10-17, the day their key compromise was reported, and scheduled the removal of their root, emSign ECC Root CA - G3, from the Chrome Root Store for 2026-10-27. gstack's default headless browser gets neither: it launches with Playwright's `--disable-component-update`, so it receives no CRLSet updates, and the root store built into the bundled Chromium still trusts the root. On 2026-10-24, `$B goto https://dv-g3b-emudhra.we-have-a-problem.com/` loaded the test page on gstack's pinned Playwright 1.62.1 (Chromium 151.0.7922.34), and Chrome for Testing builds from 158.0.8115.0 fail it with `ERR_CERT_AUTHORITY_INVALID`, so the Playwright bump waits for a release that bundles one of those builds. + +### Changed +- **`BROWSER.md`**: a new "TLS trust: the bundled Chromium's root store" section, at the end of the security stack, gives the warning, the plan (move to the first Playwright release whose Chromium is 158.0.8115.0 or later; until then, no signed-in work in this engine on an untrusted network) and the check: on a network that does not inspect TLS, after `$B stop` and once a known-good HTTPS site loads in headless mode, only `ERR_CERT_AUTHORITY_INVALID` on the test host means a build is fixed. Environment-dependent caveats (proxy variables, profiles, custom builds, headed CRLSet timing) are tracked in #3440. The table of contents, the fallback-engine introduction and the `cookie-import-browser` and `handoff` command rows link to the section. Aside is unaffected, because it is your own browser with its own updates. + +### For contributors +- The edits above the cookie workflow excerpt change no line count, so the `setup-browser-cookies/SKILL.md workflow` judge input is byte-identical (prompt SHA-256 `c851e635…`, 12,805 bytes, before and after). + +## [1.91.238.0] - 2026-10-26 + +**Five /ship frictions the unattended lanes hit (GSTA-102).** Each fix is one commit. + +### Fixed +- **`gstack-gate-diff ` in a clone with no local `` branch.** Before, `gstack-gate-diff main` in a checkout that cloned only its feature branch printed `GATE_ERROR=no_base ref=main fix=git fetch origin main`, and that fix did not help: the tool only tried the local branch, and in a single-branch clone a bare `git fetch origin main` updates FETCH_HEAD alone. Now `lib/gate-diff/git.ts` resolves the base to its remote-tracking branch (`origin/main`, origin first, then the other remotes) when no local branch exists, says so once on stderr, and the merge-base is the same commit. When the remote-tracking branch is missing too, the fix line fetches into it explicitly: `git fetch origin +main:refs/remotes/origin/main` (with `--unshallow` on a shallow clone), which works in every clone shape. `test/gate-diff-cli.test.ts` adds a single-branch clone fixture with no local `main` and no `origin/main`: it asserts the error, runs the printed fix verbatim, and expects the next run to list the fixture's RH-1 hunk; two more clones cover a second remote that carries the branch when origin does not, and an upstream-only clone of a master-default repository whose default base (no argument) now comes from the same remote list as the fix line. The refspec is forced (`+main:refs/remotes/origin/main`) so the line also repairs a tracking ref that exists but shares no history with HEAD. +- **`gstack-post` no longer flags the repository's own release versions as public IPs.** A four-part version is byte-for-byte a dotted quad, so "(1.91.191.0)" in a PR body raised `pii.ip_public` (MEDIUM) and stopped an unattended docs audit that cannot confirm a MEDIUM finding. The engine's version exemption only reads the declaration right before the span (`"version": "`, `## [`), so prose that merely names a release got no help. Now `lib/gstack-post.ts` reads the root `VERSION` value and every version in a root `CHANGELOG.md` release heading (`## [1.2.3.4]`, `## v1.2.3.4`, through the same `parseChangelog` gstack-restamp uses) and passes those exact strings to the scan's allowlist. Verdict: this narrows one detector by the repo's own release set, and nothing else. The allowlist is exact-string and context-free, so a released quad is exempt wherever it appears in the text, `host: 1.91.191.0` included; the set is the repo's own releases and nothing wider. `8.8.8.8` and `203.0.113.7` in prose, a quad the repo never released (`1.128.1.0`), and the same text outside a repository or in a repo whose VERSION is three-part all still need confirmation; `test/gstack-post.test.ts` carries those controls beside the four benign bodies (`(1.91.191.0)`, `v1.91.226.0`, a bare `1.91.191.0`, `release 1.91.190.0`) against a synthetic CHANGELOG in which `1.91.192.0` is not a release. The same post path also stops reading a ship receipt's all-digit tree hash (`tree=704992822996`) and an 11-digit GitHub Actions run id in prose or link text (`run 19368712345`, `[run 19368712345](…/actions/runs/19368712345)`) as `pii.phone.e164`: only the exact tokens in those positions are allowlisted, and `+14155551234`, a bare 11-digit number and a 10-digit number after `run` still need confirmation (GSTA-94 hit this on #3424). +- **`gstack-evidence` records the test count of gstack's shard runner and of the remote pregate lane.** `bun run test:free` / `test:ubicloud` run quiet shards that forward bun's `Ran N tests` line without its ` N pass` counts, so the bun parser read every full-suite run as `tests_ran: 0` and /ship's Step 16 saw a zero-run suite (GSTA-94 on #3424); the remote lane prints `PREGATE_REMOTE: tree=… tests=9660 failed=0` (a lane row under `lanes:` in the pregate table) and `requires-remote: … cleared by remote-gate tree=… tests=9660`, which no parser knew, so it was `tests_ran: unknown` and the ship receipt and the Review Readiness Dashboard never carried the number. Now a bun terminal line with no counts block above it is not a bun summary (bun always prints ` 0 pass` / ` 0 fail`, even for an all-skip run, which still grades ZERO-RUN), the shard runner's per-shard `[test:free] PASS — N tests` epilogues are summed for the last run in the output (a files-from run's `FILES-FROM RESULT: … tests=N` is the same sum; a run with a `FAIL —` or `FLAKY-PASS —` epilogue and no such total has lost a shard's count and is recorded as unknown, not a partial sum), and when the shard runner spoke its verdict stands for the whole tail (the tail is the stdout tail then the stderr tail, so a line's position cannot order bun's stderr counts block against the runner's stdout epilogues; a focused `bun test` run before the suite in the same command no longer records its count, or ZERO-RUN, for the 10k-test run that followed), and the remote lane's two lines are parsed as whole lines like every other runner pattern (a prefixed or quoted echo does not count; the later line wins when both appear); a paid-only selection's `tests=0` is ZERO-RUN, not a pass. Honest limit: the shard runner and the remote gate print only bun's terminal totals, which include skipped tests, so for those two the number is tests ran with skips included, not executed (the shard runner refuses a shard whose bun summary counts fewer files than planned, but a shard whose files all loaded and executed nothing still passes with that total). The exit code is still never changed by the parse. `test/evidence.test.ts` runs the wrapper over `test/fixtures/evidence/pregate-remote-failed.out`, the stdout of a real `gstack-pregate --stage tests --remote` run captured under gstack-evidence on 2026-10-26 (9658 tests, 1 failed, recorded as `tests_ran=9658` with `exit=1`, never FRESH), over a cleared lane's table (`tests_ran=9660`, FRESH), over the receipt line alone, over the two lines with differing counts in both orders, and over `test/fixtures/evidence/shard-runner-files-from.out`, the remote gate's own shard-runner stream for this branch (275 files, 16 shards, 10138 tests: the epilogue sum equals the `FILES-FROM RESULT` total, with and without that final line, and a forwarded `Ran N tests` line on stderr no longer zeroes it). +- **`gstack-restamp` refuses only the CHANGELOG duplicates the PR introduces.** `CHANGELOG_DUPLICATE_HEADING` compared the branch's headings with themselves, so a base branch whose released history already heads one version twice (gbrain's `master` carries two `## [0.38.0.0]` sections, and its own check requires every one of them to stay) could never be stamped. The check now takes the base branch's CHANGELOG, read at `origin/` and at the commit the branch's own lines are counted from (its fork point, or the predecessor's head in a gate-ahead stamp), the larger count per heading winning: a heading the base carries N times may appear N times on the PR, and only a further copy is refused, with a message that says how many the base has. When a reading is unavailable (the PR adds the CHANGELOG itself) the stage detail and the refusal name it. A duplicate the PR adds, on a clean base or beside an inherited one, is still refused with nothing written. Tests: a fixture whose `main` heads `1.2.2.0` twice stamps and keeps both copies, and still does after `main` collapsed the copies behind the PR's back; a PR that adds a third copy, one that adds the first duplicate on a clean base, and one whose new CHANGELOG on a base without one heads a version twice, are refused. `docs/troubleshooting.md` says which duplicates count. +- **`gstack-restamp --next` under a train no longer asks the lane to edit the CHANGELOG.** Its `next:` line told every stamp to "write the CHANGELOG entry text"; a lane that obeyed on #3422 added a release summary under the new heading, the train's verify refused the head (the CHANGELOG must equal the base's plus the PR's own entry re-headed) and the queue froze. Under `queue_mode stamp-at-merge` the line now says to commit and push only the stamp diff with no edit of the lane's own; the claim-at-open wording is unchanged, since that bump still writes its entry at /ship's CHANGELOG step. Test: the stamp-at-merge fixture's output carries no instruction to edit the CHANGELOG by hand, and the claim-at-open fixture still does. + +## [1.91.237.0] - 2026-10-26 + +**An unstamped PR can change a release output again (GSTA-96 on #3434).** Regenerating `agents-digest/gstack-AGENTS.md` after a generator change made the Version Gate classify the head as stamped (the digest is in `release_outputs`) and demand base + bump, while changing only `scripts/gen-agents-digest.ts` failed `test/agents-digest.test.ts`'s freshness check, so a digest rewording could not land before its slot. + +### Fixed +- **Version Gate under a train policy** (`lib/version-gate-train.ts`, `scripts/compare-pr-version.ts`): a changed `release_outputs` file no longer counts as a stamp when it is byte for byte what the policy's `release_tool` generates at the head's merge base's `VERSION` (the head's own, since `VERSION` is unchanged on this path; the base tip may have stamped since, and CREW rule 14 says not to merge it); the gate runs the head's tool in a throwaway worktree with that `VERSION` (the changed outputs removed first, a bare environment with `GSTACK_RESTAMP_VERSION` and `GSTACK_RESTAMP_STAGING=1` and none of the gate's tokens, no `node_modules` link, a 120 s limit enforced with SIGKILL so a tool that ignores TERM cannot hold the gate open, output read from a file, first 4 KB, so a lingering child cannot either) and compares byte for byte. `VERSION` and `package.json` version changes still classify the head as stamped, as does an output that differs (hand-edited, generated at a bumped version, or never written by the tool) or a policy with no `release_tool`; a tool that exits non-zero, is killed or cannot be staged is an error with the reason, never a pass; the staging worktree is removed and pruned on every path. `version-gate.yml` checks the head out with `persist-credentials: false`, since the head's tool now runs inside the gate. The freshness test stays strict and a stamp commit still changes only the stamp files, since the slot's restamp regenerates the output. `docs/ship-policy.md` states the rule under "Lane: report ready". + +### Tests +- `test/compare-pr-version.test.ts`: a generator change with its regenerated output at the merge base VERSION passes as unstamped (red without the fix), and stays unstamped after main stamps again; a tool whose background child outlives it does not hold the gate open, nor does one that traps TERM (`regeneratedAtBase` is exported for that test); a hand-edited output, an output generated at the bumped version, an output the tool never writes, a VERSION bump without the output, a failing release tool and a policy without `release_tool` all still refuse; the tool sees no `GH_TOKEN`; no worktree is left behind. + +## [1.91.236.0] - 2026-10-26 + +**The merge queue now refuses a PR that never ran /ship.** Garry, 2026-10-25: "we should really be using the gstack ship skill before merging PRs." `gstack-ship-queue ready` printed `SHIP_RECEIPT: none in the PR body` as information and returned ok, so every PR in the pool was READY'd without a /ship run. The receipt /ship writes into the PR body is now a precondition at `ready`, `train build` and `verify --train`, with no flag around it. + +### Changed +- **`gstack-ship-queue ready`** refuses with `QUEUE_RECEIPT_MISSING` when the PR body has no valid ```gstack-ship-receipt``` block and with `QUEUE_RECEIPT_STALE` when the receipt's `head` is not the head being READY'd (or its `pr` is another PR). The re-READY after a slot's stamp push accepts the receipt written for the pin when HEAD is the pin plus stamp files, or (from slot 2 on) the pin merged with main plus stamp files. The informational `SHIP_RECEIPT:` line still prints in the block, and it is one line whatever the receipt's strings contain (control characters are collapsed, so a crafted `base` cannot add a forged `MERGE_CONDITION:` line to `verify --train`'s output). +- **`gstack-ship-queue ready --screen` refuses a stale merge ref** with the new `QUEUE_SCREEN_STALE_REF` (GSTA-95 on #3431): GitHub stops recomputing `refs/pull//merge` while a PR conflicts with the base (a CHANGELOG-only conflict after a stamp counts), so a screen dispatched there planned 0 cases, reported success and `ready` printed `SCREEN_DISPATCH:` as if a screen ran. `ready` now compares the merge ref's head parent with the PR head before dispatching (a missing merge ref carries the same code), never adopts a finished screen that planned no cases (a superseded run skips plan-slices and uploads no `paid-plan` artifact; a manifest with nothing planned counts the same), and `train build` prints `SCREEN: # run planned no cases …; counted as not-run` and seats that slot unscreened (a plan it cannot read keeps the slot screened and prints `… counted as screened, unverified`). +- **`gstack-ship-queue train build`** refuses a pin whose PR has no current receipt with the same codes, naming the PR, before any write, push or dispatch, `--dry-run` included. **`verify --train`** refuses the slot the same way before `MERGE_CONDITION`, and accepts a receipt written for the slot's pinned head when the current head is its stamp commit (the stamp changes only stamp files, or merges main into the pin from slot 2 on), since no /ship runs on a stamp. +- **`gstack-ship-receipt result`** (new) is /ship's terminal step: it binds the posted receipt to HEAD, writes `/ship/runs/-/{ship-run.json,ship-receipt.md}` (a /ship record, not a `run` v1 manifest: `gstack-artifact validate` does not apply) and prints `GSTACK_RESULT: skill=ship status=complete run=`, `status=refused run=-` with the receipt code, or `status=incomplete run=-` when gh cannot read the PR, so a parent's merge check reads `ship-run.json` instead of parsing the PR body. The ship skill's new Step 22 runs it after the pr-body and handoff sections. +- **Unattended /ship may update an existing PR's body.** `gstack-skill-start`'s unattended block and `UNATTENDED_SHIP:` line, the ship skill's Step 18 and `docs/unattended.md` now say: when an open PR for the branch exists and HEAD is its pushed head, Step 18 takes the pr-body section's existing-PR path (body with the receipt first, and title); it never runs `pr-create` and never pushes a commit; anything else takes the handoff section. Before this the block said the receipt lived only in `.gstack/tmp/ship-receipt.md` while rule 22 and `ready` read it from the PR body. +- **`INSTALL_UNVERIFIED` with no verdict names the directory to install from.** An installer run from the skill's own folder records a verdict for no project, so /ship launched from the project found none and the fix said only "run the installer on this machine" (GSTA-85). Both twins (`lib/install-verdict.ts`, `bin/gstack-install-verdict.sh`) now say `run … scripts/install.sh --for ship from : the verdict is keyed by the project directory the installer runs in`. +- **Docs:** `docs/ship-policy.md#ship-before-ready` (the lane runs /ship before every READY and after every push; the stamp keeps the pin's receipt; the receipt is a process gate, not an authenticity check), the three codes in `docs/troubleshooting.md`, `docs/unattended.md` (/ship now prints the result line), and the ship skill's pr-body section says what refuses without a receipt. +- **Gate edit (size budgets):** `test/helpers/carve-guards.ts` raises ship's `maxSkeletonBytes` 81,000 → 81,600 and `maxSizeRatio` 1.6690 → 1.6740 for Step 22 and the Step 18 routing, offset in part by tightening Steps 14, 16, 18 and 20 prose (measured skeleton 81,535, union 314,027; the 2026-10-14 tightening had left 2 bytes of headroom). No threshold, trial count or case kind changed. + +### Tests +- `test/ship-queue-train.test.ts`: READY, train build (dry run and live) and verify --train refuse a missing, invalid, other-head and other-PR receipt and admit the current one; the pinned-head receipt admits a stamp commit; verify refuses `QUEUE_PROOF_MISSING` when gh cannot read the PR; `--screen` refuses a lagging or missing merge ref and never follows a 0-case screen. The train fixture's `refs/pull//merge` is now a real merge commit (`mergeRefCommit`), as GitHub keeps it. `test/gstack-ship-queue.test.ts`: `ready` at a stamp commit (slot 1, and slot 2 with main merged in) accepts the pin's receipt; a bare merge of main with no stamp on top, an empty commit over the merge, or an edit on the pin side under the slot-2 stamp, is `QUEUE_RECEIPT_STALE`. `gstack-ship-receipt result --pr ` refuses `QUEUE_RECEIPT_STALE` when HEAD is not the PR's pushed head and prints `status=incomplete` when gh returns no head or the run record cannot be written; `ready --screen` names a failed `ls-remote` or fetch of the merge ref instead of blaming a conflict. Both install-verdict twins tell an unreadable verdict file from a missing one. `test/auq-error-fallback-hook.test.ts`: the unattended directive names the same `GSTACK_RESULT` writers as docs/unattended.md and `gstack-skill-start` (GSTA-91). `test/gstack-ship-receipt.test.ts`: the `bindReceipt` head-matching table (12-character floor, other PR, one-line `SHIP_RECEIPT:`), `result --body` refusals, `--head`, a non-git cwd and gh failures. The train fixture's `openPr` now writes a receipt for the head it opens (`shipped: false` leaves the body empty). `test/install-verdict.test.ts` pins the GSTA-85 fix in both twins; `test/gstack-skill-start.test.ts` pins the new unattended /ship block. + +## [1.91.235.0] - 2026-10-26 + +**An issue we intend to fix later now stays open under the `parked` label instead of being closed.** The Oct 11 triage closed 75 feature requests with a "not rejected" note, and GitHub showed them as closed with no fix. Those 75 are open again under the new `parked` label on garrytan/gstack ("Not fixed yet, not rejected: stays open; a comment says where it is tracked and what fixes it"), each with one comment saying where the work is tracked and what fixes it. The rule is written down: an issue closes only as fixed, duplicate or won't fix, each with its reason; a report that main already fixed closes as fixed, not won't fix; and a duplicate uses GitHub's own duplicate close reason. + +### Added +- **`docs/triage.md`**: the three issue states (open, parked, closed), how to park an issue (the label plus one comment naming where it is tracked and what fixes it), when the label comes off, the three close reasons with their GitHub close reason and label, and where the parked themes live. + +### For contributors +- **`CONTRIBUTING.md`**: an "Issue triage" section ahead of the PR wave process, pointing at docs/triage.md. +- **`TODOS.md`**: the Wave 9 feature-themes note says issues stay open under `parked` and points at their theme there (73 listed, 75 with #3061 and #3122), instead of closing with a link. + +## [1.91.234.0] - 2026-10-26 + +**Weekly tagged releases, and a `stable` channel that team-mode auto-update follows by default.** Main kept stamping a version per merge, a dozen a day, with no tag or release anywhere, and `./setup --team` installs moved to every one of them within the hour. Now a Monday-morning workflow tags the newest green commit on main as `v`, publishes a GitHub Release from the CHANGELOG headlines and fast-forwards a `stable` branch to it; team installs follow `stable` and move once a week (plus hotfixes), contributors keep following main, and one config key switches either way. Nothing about per-merge stamping or the merge train changed (docs/releases.md). + +### Added +- **`.github/workflows/weekly-release.yml`**, **`scripts/release-cut.ts`**: every Monday at 14:00 UTC, or by `workflow_dispatch` (`dry_run`, default true for a manual run; `ref` for a hotfix), the newest first-parent commit on main whose `free-tests` and `main-backstop` check runs succeeded, with every check run finished and none red and no active freeze in `refs/notes/gstack-trains`, is tagged `v`, released with one headline per CHANGELOG version since the previous release (capped at 60 KB), and becomes `refs/heads/stable`. Stable only moves forward along main; a candidate behind it, a tag naming another commit, or a frozen queue is `REFUSED`. A cut that stopped half-way (tag and `stable` pushed, release not created) is finished by the next run. A dry run prints the plan and the exact commands and changes nothing (nothing green is exit 0 for a dry run, with every skip named); a pull request that touches the workflow or the script runs that dry run against main as a read-only job. The cut job runs only from `refs/heads/main`, takes `ref` as a full 40-hex SHA, and has `contents: write` and `checks: read` only; both rulesets cover `refs/heads/main` alone, so the token can push the tag and `stable`. +- **`.github/workflows/evals-main-backstop.yml`**, **`scripts/train-backstop.ts`**: the backstop's verdict is published on the commit as the `main-backstop` check run from a code-free `checks: write` job (`--outputs` records the action): success for `covered` and `gated`, neutral for `superseded`, `deferred` and `inert`, failure for `frozen` and `failed`. The run's own conclusion is exit 0 for a superseded push (main moved past it before it was gated), so reading the run would have called a never-gated tree green; the release reads the check run instead. +- **`bin/gstack-update-channel.sh`**: resolves the channel (`gstack_update_channel`: `update_channel` `auto` is `stable` when `team_mode` is true, else `main`; `GSTACK_UPDATE_CHANNEL` overrides), and as a command (`--fetch-ref`) fetches the channel ref and prints `origin/stable` or `origin/main` for the /gstack-upgrade skill. +- **`bin/gstack-config`**: new key `update_channel` (`auto` | `stable` | `main`), validated on `set`, in the header, `list` and `defaults`. +- **`docs/releases.md`**: what a release is, what green means, the channels and how to switch, hotfixes, dry runs and every outcome line. + +### Changed +- **`bin/gstack-session-update`**: on the stable channel it fetches `+refs/heads/stable:refs/remotes/origin/stable` (team installs are single-branch clones) and fast-forwards to it, logging `CHANNEL stable target=`. Before the first release exists it follows main (`CHANNEL_FALLBACK stable->main`). An install already past the release stays put (`CHANNEL stable ahead_of_release`, then `UP_TO_DATE`): a depth-1 clone cannot relate the two commits, so the VERSIONs decide. The main channel's fetch, target and log lines are unchanged. +- **`bin/gstack-update-check`**: reads `/VERSION`, resolves the SHA with `ls-remote refs/heads/` and compares against it; the cache identity carries `#stable` so a channel switch never replays the other channel's outcome. With no `stable` on origin yet it follows main for that check, as the session updater and `gstack-upgrade` do, instead of caching `CHECK_FAILED` from a 404 until the first cut. +- **`bin/gstack-upgrade`**: fetches and fast-forwards to `stable` on that channel (`gstack-upgrade: channel stable ...` on stderr), follows the default branch with a note when origin has no `stable` yet (an `ls-remote` that fails is a fetch failure, not an empty answer), and treats a release whose VERSION is not higher than the installed one as current instead of diverged in a shallow clone. `--check` output lines are unchanged. +- **`gstack-upgrade/SKILL.md.tmpl`**: Step 4 resolves `UPSTREAM_REF` through the helper and uses it for the Bun-floor check, the fast-forward, the unpushed-commit gate and the reset fallback instead of a literal `origin/main`; the fallback block requires the carried `UPSTREAM_REF` (`:?`) instead of defaulting to `origin/main` in a fresh shell, and a team install already past the release stops with `AHEAD_OF_STABLE` instead of `FF_REFUSED` and an offer to reset backwards. The helper's `--fetch-ref` keeps `origin/stable` when the fetch fails for any reason other than a missing release, so a network blink never moves a team install to main; its unused `gstack_update_channel_ref` is gone. +- **README** Step 2 (team mode) says team mode follows `stable` and how to follow every merge instead. + +### Tests +- `test/release-cut.test.ts`: on a real bare origin with a fake `gh`, the newest green first-parent commit wins and every skip is named (pending or neutral `main-backstop`, red check run, missing or running `free-tests`, no `main-backstop` check run, an unrelated check run still going); an active freeze refuses and a cleared one does not; a cut creates the annotated tag, `stable` and the release with `--verify-tag` and the headline in the notes; re-running is `NOTHING_NEW`; a cut whose `gh release create` failed is finished by the next run with notes against the release before it; the CLI exits 0 on a dry run with nothing green and 1 on the real cut; a hotfix `--ref` promotes a later main commit with a compare link; stable never moves backwards; a side-branch ref and a clashing tag are refused; a thousand-version span stays under the notes cap. +- `test/update-channel.test.ts`: the resolution matrix (team mode × key × env override); the session-start hook on a depth-1 team install moves to stable (B) not main (C), follows main on `update_channel=main`, falls back to main with no stable on origin, and stays put when already past the release with nothing pending; `gstack-upgrade --check` and the upgrade follow stable then main after the key flips, say when no release exists, and call a depth-1 install past the release current; `gstack-update-check` on stable stays silent while main is ahead, asks `ls-remote` for `refs/heads/stable`, carries the channel in its cache identity, and follows main when origin has no stable yet; `--fetch-ref` keeps `origin/stable` when origin is unreachable. +- `test/weekly-release-workflow.test.ts`: the workflow's triggers, concurrency, the cut job's main-only guard, permissions and 40-hex `ref` check, and the read-only pull-request dry run. `test/evals-train-workflow.test.ts`, `test/train-backstop.test.ts`: the `backstop-check` job's action-to-conclusion map, its code-free `checks: write` split, and the CLI's `--outputs` line. +- `test/upgrade-template-pins.test.ts`, `test/gstack-upgrade-fresh-shell.test.ts`: the pinned fast-forward, unpushed-commit gate and reset markers name `"$UPSTREAM_REF"` instead of `origin/main`; the ordering invariants are unchanged. + +### Gate integrity +- No threshold, budget, timeout, trial count, case kind or assertion changed. The release workflow reads check runs and the trains notes ref; it dispatches nothing and runs no tests or evals. The backstop gates exactly as before; it additionally publishes its verdict as a check run. + +## [1.91.233.0] - 2026-10-26 + +**/plan-tune's inferred profile no longer pins at 1.0 after a few reviews, and it now records how many answers moved each dimension.** This closes the last two parts of #2623. One /plan-eng-review produces about five answers, and every answer added its full delta, so eight sessions of "fix now" took `architecture_care` to 0.998, as uninformative as the 0.5 it replaced. Now each UTC day moves a dimension at most as far as one strong answer, and eight days of the same answers read 0.808. The profile JSON (`--profile`) also records `contributing_events` per dimension, so a dimension with no evidence reads 0 there instead of a bare 0.5; /plan-tune's display and `--check-mismatch` don't read it yet (#3436). + +### Fixed +- **`bin/gstack-developer-profile`**: `--derive` sums each UTC day's deltas per dimension (by the row's `ts`) and clamps the sum to the largest single delta in the signal map (±0.06 today). Answers within a day net out before the clamp. Only an ISO 8601 date or date-time `ts` counts as dated, and a time without a zone is read as UTC; every other row shares one undated bucket, so a junk timestamp can't escape the bound. `diversity.days_span` counts the same UTC days. The bound ignores `session_id` on purpose: the PostToolUse hook and the agent log different ids for one review, and an unsubstituted `SESSION_ID` placeholder would merge every review into one bound or leave each answer unbounded. Several reviews on one day count as one. `--trace` uses the same accumulation, so it lists every contributing answer, each clamped day ("day 2026-04-01: +0.250 capped to +0.060") and a `TOTAL` line whose value matches `--derive`. +- **`bin/gstack-developer-profile`**: a logged choice named after an `Object.prototype` member, such as `constructor`, or a log line that parses to a non-object such as `null`, no longer makes every later `--derive` and `--trace` fail; signal lookups use own properties only. `--trace` replaces control characters in the logged fields it prints, so a logged `ts` can't forge a `TOTAL` line. + +### Added +- **`inferred.contributing_events`**: per dimension, the number of logged answers that moved it (an answer that writes two deltas to one dimension counts once). Ported from #2625 by @v639dragoon, with co-author credit. #2625's autonomy signal from `followed_recommendation` stays parked, as decided in its Oct 8 review. + +### Docs +- `docs/designs/PLAN_TUNING_V0.md` documents `signal_events`, `contributing_events` and the per-day bound in the profile schema, including why the bound ignores `session_id`. +- `TODOS.md`: the #2623 entry now covers only its remaining Gate 1 work, the `{skill}-{slug}` hinted fallback, tracked as #3442. + +### Tests +- `test/gstack-developer-profile.test.ts`: `contributing_events` counts unique rows when one answer writes two dimensions, and is all zero for an answer that matches no signal. The bound equals the signal map's largest delta. Five answers on one day move a dimension by exactly the bound in both directions, whether they carry one `session_id`, five different ones, the literal `SESSION_ID` placeholder, or one review logged by both the hook and the agent. The day is the UTC day of `ts` even when the machine's zone is west of UTC, and a `ts` without a zone, `T`- or space-separated, is read as UTC. `--trace` prints the clamped days and a total that matches `--derive`, and replaces control characters. A `constructor` choice, a `null` line and array-valued `question_id` or `user_choice` rows are ignored. Rows whose `ts` is not ISO 8601 (`TS`, `junk 1`, a number) share one undated bucket and add no days to `days_span`. Negative controls: the same answers on separate days, a second day after a capped first day, and an answer pair that nets out are not clamped. The original positive tests fail against the old bin; the day-bound, `constructor` and control-character tests fail against the per-session version; a one-sided clamp fails the bound tests, and local-day bucketing fails the UTC tests. +- The test helper `logQuestion` sets `GSTACK_QUESTION_LOG_NO_DERIVE=1`, so the background derive can't race a test's own `--derive` (from #2625). + +### Gate integrity +- No test expectation, threshold, budget, timeout, trial count or case kind changed. + +## [1.91.232.0] - 2026-10-26 + +**The setup-browser-cookies workflow judge now has a preregistered calibration corpus, and its structured-output calibration is recorded as held, so the judge stays on today's request (GSTA-91).** In train-4 gate 40173759210, the cookie judge (claude-sonnet-5-5, free-text transport) returned JSON with an unescaped quote. The repo's fix for that shape is the JSON-schema transport, and `test/fixtures/judge-calibration/README.md` lets a configuration switch only after a calibration lands. The cookie configuration had no corpus until now. Its comparison-1 run removed the parse errors (old 2 of 144 samples, new 0 of 72) and matched or improved every other measure. It is held on one landing condition: a new false pass on a07. a07 scored 4/4/4 on all three new samples and on all three `old-b` samples. `old-a`'s a07 panel is incomplete because one old sample returned invalid JSON (both valid `old-a` a07 samples also scored 4/4/4), and false passes are counted against `old-a` alone, so the result counts against the new request only. + +### Tests +- `test/fixtures/judge-calibration/cookie-workflow/`: 24 items. p01 is the production bundle (`buildCookieWorkflowJudgeInput`); a02 and a05 are real earlier revisions; the rest are hand-edited variants rendered by the same builder. Labels were preregistered and committed before any request with the schema was sent. A separate run (openai/gpt-6.1-sol) then labeled every item blind, and a third reviewer (google/gemini-4-argon) adjudicated the two disagreements: a01 kept pass, a08 changed to fail. `runs/` holds every sample and the result. The cost was 216 calls and $2.81, priced in `manifest.json` before dispatch as a separately funded entry. +- `scripts/judge-calibration-configs.ts`: the `cookie-workflow` configuration builds prompts with `buildWorkflowJudgePrompt(COOKIE_WORKFLOW_JUDGE, …)` and uses the eval's median floors of 4/3/4. Its new request differs from the old one only by `WORKFLOW_JUDGE_RESPONSE_SCHEMA`. +- `test/judge-calibration.test.ts` covers the new configuration and its pass rule, including the actionability floor. The wiring test now requires the cookie registration to send the schema exactly when the recorded cookie calibration lands. It doesn't land, so the registration stays unchanged and 15 workflow registrations use the schema. The cookie registration may only spread `COOKIE_WORKFLOW_JUDGE`, so the calibrated request stays the one production sends. +- Two new free tests: the calibration builds byte-for-byte the prompt the cookie eval sends, and `analyze()` reproduces the committed result from its labels, samples, stop record and request options. + +### Gate integrity +- No judge request, rubric, threshold, sample count, landing rule or case changed. The cookie judge keeps its free-text request. No gate exception is proposed. + +## [1.91.231.0] - 2026-10-26 + +**A fresh install is about 520 MB smaller: `./setup` installs production dependencies only.** Setup ran `bun install` without `--production`, so every user also got the test and eval toolchain from `devDependencies`, mostly the Claude Agent SDK's native binaries (both the glibc and musl builds on Linux, ~440 MB). Measured on a clean Ubuntu 24.04 container with a cold Bun cache (4 vCPU, two runs each): `node_modules` goes from 1,094 MB to 572 MB, the checkout from 1.74 GB to 1.22 GB, and the whole install with Chromium from 2.42 GB to 1.90 GB. `./setup` took 24.6 s and 20.0 s before, 21.1 s and 18.9 s after. + +### Changed +- **`setup`**: every dependency install (before the build, the Codex render and each host render) goes through one `install_runtime_deps` function: `bun install --production --frozen-lockfile`. Bun treats `--production` as frozen, so when `bun.lock` is behind `package.json` (a fork or a local edit) it refreshes the lockfile with `bun install --lockfile-only` and installs production-only again. **`bin/gstack-browser-ensure`** installs the same way when `node_modules` is missing, and its failure line suggests the production install rather than a full `bun install`. +- **`package.json`**: `@anthropic-ai/sdk` (loaded by `gstack-distill-free-text`, /plan-tune's dream cycle) and `xterm` and `xterm-addon-fit` (copied into the browser extension by `bun run build`) move to `dependencies`, because a user install runs them. The Claude Agent SDK, TypeScript, Prettier and `@types/bun` stay dev-only. +- **Contributors and CI keep the full install.** `./setup` treats a checkout that already has the dev toolchain (`node_modules/typescript`) as a contributor's and runs the full `bun install --frozen-lockfile` there, so dev packages keep following `bun.lock` after a pull (a production install would report "no changes" and leave them at old versions). `bin/dev-setup` now always runs `bun install` (it skipped it when `node_modules` existed, which would have left a checkout first set up by `./setup` without the dev toolchain). CI workflows already run `bun install --frozen-lockfile` themselves. CONTRIBUTING.md says which command installs what. + +### If you already have gstack installed +- An existing install keeps its dev dependencies and keeps getting the full install, because it already has the dev toolchain. To get the space back: `cd ~/.claude/skills/gstack && rm -rf node_modules && bun install --production --frozen-lockfile && rm -rf "$(bun pm cache)"/@anthropic-ai/claude-agent-sdk*`. The last step matters: Bun hardlinks `node_modules` into its global cache, so the Agent SDK's binaries stay on disk until their cache entries go. Don't do this in a checkout you develop gstack in. + +### Tests +- `test/setup-production-deps.test.ts`: `install_runtime_deps`, sliced out of `setup` and run against a recording `bun` that models Bun's frozen `--production`, passes `--production` on the frozen-lockfile call, refreshes a stale lockfile before retrying, and stops setup when the refresh fails. A checkout with the dev toolchain gets the full frozen install, and a failed full install stops setup too. A walk of `bun.lock`'s production closure (with required-peer and optional-peer controls) proves no production dependency brings in `typescript`, the marker setup checks. The real-Bun rows run with `HOME`, `XDG_CONFIG_HOME` and `BUN_CONFIG_*` isolated, so a developer's global `.bunfig.toml` cannot change them. Run with the real Bun on local `file:` packages: a fresh install leaves out devDependencies, a later production install keeps the ones a full install added, and a stale lockfile is refreshed while the install stays production-only. No other line in `setup` calls `bun install` (the pre-change line is the negative control). `bin/dev-setup`'s install block, run over an existing `node_modules`, still calls plain `bun install`. A scan of every shipped file (`bin`, `lib`, `browse/src`, `design/src`, `make-pdf/src`, `hosts`, `extension`, the resolvers, and the scripts `setup` and the build call by path) finds no import or `require` of a package outside `dependencies`. Modules the shipped code reaches by relative import (gen-skill-docs' `scripts/` helpers, and the `test/helpers/providers` modules `gstack-model-benchmark` loads) are scanned too. Negative controls: a planted Agent SDK import is caught, including one split over several lines and a re-export; a type-only import is not; and moving `@anthropic-ai/sdk` back to dev flags `gstack-distill-free-text`. The packages `bun run build` copies out of `node_modules` are production dependencies. +- `test/gstack-browser-ensure.test.ts`: with `node_modules` missing, the preflight runs the frozen production install, refreshes a stale lockfile before retrying, and ends a failed install in `BROWSER_UNAVAILABLE` with a production-install fix line. +- The install fixtures' stub `bun` (`test/helpers/install-fixture.ts`, `test/helpers/claude-overlay-fixture.ts`, `test/team-mode.test.ts`) now matches `install --production --frozen-lockfile`. The team-mode stub fails on any other argv; the two helper stubs hand other commands to the real `bun`, so for them the new argv is a match, not a regression check. + +### Gate integrity +- No threshold, budget, timeout, trial count, case kind or assertion was weakened. The four stub patterns (in three files) changed to the new argv only. + +## [1.91.230.0] - 2026-10-26 + +### Changed +- **The archetype the user picks on the /design-shotgun board now reaches /design-html.** Captions name the archetype id (`classic-hero`, `long-form-story`, `minimal-conversion`, `comparison`). On approval, approved.json also gets the chosen `archetype` and its `sections` outline, added with the Edit tool so no free text reaches a shell command. /design-html reads both in Case A, and the craft section uses them as given instead of choosing again or asking. +- **Accepted craft exceptions persist.** /design-html's finalized.json records each accepted `CRAFT_CHECK` finding in `craft_exceptions` (`id`, `count`, `reason`) and carries forward earlier ones. A later run passes that file with `--accepted`, so a finding the user already accepted is reported as accepted instead of flagged again. A higher count is still flagged as a new issue. + +### Added +- **`gstack-design-craft --accepted `**: a finding whose id an exception covers, at no more than the accepted count, prints `CRAFT_ACCEPTED: ` and doesn't fail the check. `CRAFT_RESULT` adds the accepted count. Exit codes: 0 when only accepted findings remain, 1 for unaccepted ones, and 2 for an unknown argument or a missing, unreadable or malformed exceptions file. A bad exceptions file still gets the full check with nothing accepted, after a warning, so a broken record can't switch the craft check off. A finalized.json with no `craft_exceptions` field, written before this change, means no exceptions. + +### Tests +- `design-html-slop-gate` is now selected by changes to `lib/design-craft.ts` and `bin/gstack-design-craft` (`test/helpers/touchfiles-data.ts`), which it already exercised but did not list. +- `test/design-craft.test.ts` also covers accepted exceptions: count coverage and overflow, duplicate ids carried forward (the highest count wins), a bad exceptions file still checked in full, the finalized.json block in design-html/SKILL.md parsed by the same reader the CLI uses, rejected malformed lists, CLI exit codes with `--accepted`, and the archetype handoff contract (the same four ids in /design-shotgun and the craft section, the approved.json step, Case A and the craft section's no-re-ask rule). + ## [1.91.229.0] - 2026-10-26 **The OpenSSF Scorecard workflow uploads its results with `github/codeql-action/upload-sarif` 4.39.0 (from 4.38.3), recreated from Dependabot's #3307.** Dependabot's branch can't take a version stamp at merge time, so the same one-line change ships from a crew branch. The new pin `6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee` is the commit that the annotated tag `v4.39.0` points to in github/codeql-action, and GitHub shows that commit as verified. 4.39.0's one breaking change raises the minimum CodeQL bundle to 2.20.7, which applies to `init` and `analyze`. This workflow uses only `upload-sarif` with Scorecard's own SARIF file. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee101e0eb..e3a9220eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,8 @@ bun install # install dependencies bin/dev-setup # activate dev mode ``` +`./setup` on its own installs production dependencies only. `bun install` (and `bin/dev-setup`, which runs it) adds the dev toolchain the tests and evals need: the Claude Agent SDK, TypeScript and Prettier. Once they are installed, a later `./setup` runs the full install too, so they stay at `bun.lock`'s versions. + > **Full clone vs shallow.** The README's user-facing install uses `--depth 1` for speed. As a contributor, use a full clone (no `--depth` flag) — you'll need history for `git log`, `git blame`, `git bisect`, and reviewing PRs against earlier versions. If you already have a `--depth 1` clone from following the README, promote it to a full clone with `git fetch --unshallow`. ### First free check (no API key, no browser) @@ -907,6 +909,14 @@ bun install && bun run build && ./setup This affects all projects. To revert: `git checkout main && git pull && bun run build && ./setup`. +## Issue triage + +An issue is open, parked or closed, and closed means fixed, duplicate or won't +fix, each with its reason in the closing comment. An issue we intend to fix +later stays open under the `parked` label with one comment saying where it is +tracked (a TODOS.md theme or a tracking issue) and what fixes it. The full rule, +including the labels and the close reasons, is in [docs/triage.md](docs/triage.md). + ## Community PR triage (wave process) When community PRs accumulate, batch them into themed waves: diff --git a/README.md b/README.md index 213da088e..eea86d521 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,8 @@ # gstack -> "I don't think I've typed like a line of code probably since December, basically, which is an extremely large change." — [Andrej Karpathy](https://fortune.com/2026/03/21/andrej-karpathy-openai-cofounder-ai-agents-coding-state-of-psychosis-openclaw/), No Priors podcast, March 2026 +gstack turns Claude Code (and other coding agents) into a virtual engineering team: a CEO who rethinks the product, an eng manager who locks architecture, a designer who catches AI slop, a reviewer who finds production bugs, a QA lead who opens a real browser, a security officer who runs OWASP + STRIDE audits, and a release engineer who ships the PR. All slash commands, all Markdown, all free, MIT license. -When I heard Karpathy say this, I wanted to find out how. How does one person ship like a team of twenty? Peter Steinberger built [OpenClaw](https://github.com/openclaw/openclaw) — 247K GitHub stars — essentially solo with AI agents. The revolution is here. A single builder with the right tooling can move faster than a traditional team. - -I'm [Garry Tan](https://x.com/garrytan), President & CEO of [Y Combinator](https://www.ycombinator.com/). I've worked with thousands of startups — Coinbase, Instacart, Rippling — when they were one or two people in a garage. Before YC, I was one of the first eng/PM/designers at Palantir, cofounded Posterous (sold to Twitter), and built Bookface, YC's internal social network. - -**gstack is my answer.** I've been building products for twenty years, and right now I'm shipping more products than I ever have. In the last 60 days: 3 production services, 40+ shipped features, part-time, while running YC full-time. On logical code change — not raw LOC, which AI inflates — my 2026 run rate is **~810× my 2013 pace** (11,417 vs 14 logical lines/day). Year-to-date (through April 18), 2026 has already produced **240× the entire 2013 year**. Measured across 40 public + private `garrytan/*` repos including Bookface, after excluding one demo repo. AI wrote most of it. The point isn't who typed it, it's what shipped. - -> The LOC critics aren't wrong that raw line counts inflate with AI. They are wrong that normalized-for-inflation, I'm less productive. I'm more productive, by a lot. Full methodology, caveats, and reproduction script: **[On the LOC Controversy](docs/ON_THE_LOC_CONTROVERSY.md)**. - -**2026 — 1,237 contributions and counting:** - -![GitHub contributions 2026 — 1,237 contributions, massive acceleration in Jan-Mar](docs/images/github-2026.png) - -**2013 — when I built Bookface at YC (772 contributions):** - -![GitHub contributions 2013 — 772 contributions building Bookface at YC](docs/images/github-2013.png) - -Same person. Different era. The difference is the tooling. - -**gstack is how I do it.** It turns Claude Code into a virtual engineering team — a CEO who rethinks the product, an eng manager who locks architecture, a designer who catches AI slop, a reviewer who finds production bugs, a QA lead who opens a real browser, a security officer who runs OWASP + STRIDE audits, and a release engineer who ships the PR. Twenty-three specialists and eight power tools, all slash commands, all Markdown, all free, MIT license. - -This is my open source software factory. I use it every day. I'm sharing it because these tools should be available to everyone. +I'm [Garry Tan](https://x.com/garrytan), President & CEO of [Y Combinator](https://www.ycombinator.com/). This is the open source software factory I use every day, and I'm sharing it because these tools should be available to everyone. The story, the productivity numbers and how I run ten sprints at once are in [Why gstack](docs/why-gstack.md). Fork it. Improve it. Make it yours. And if you want to hate on free open source software — you're welcome to, but I'd rather you just try it first. @@ -33,20 +13,16 @@ Fork it. Improve it. Make it yours. And if you want to hate on free open source ## Quick start -1. Install gstack (about 30 seconds of setup — see below) -2. Run `/office-hours` — describe what you're building -3. Run `/plan-ceo-review` on any feature idea -4. Run `/review` on any branch with changes -5. Run `/qa` on your staging URL or an isolated local API, CLI, job or webhook -6. Stop there. You'll know if this is for you. +1. Install gstack (two pastes, about 30 seconds of setup; see below). +2. Run `/office-hours` and describe what you're building. It pushes back on your framing and writes a design doc the other skills read. +3. Run `/review` on any branch with changes. It finds the bugs that pass CI but break in production, and fixes the obvious ones. +4. Run `/qa` on your staging URL, or on an isolated local API, CLI, job or webhook. It opens a real browser, reproduces bugs and fixes them. + +Stop there. You'll know if this is for you. When you want the whole sprint, `/autoplan` reviews a plan end to end and `/ship` runs tests, review and the PR. ## Install — about 30 seconds -`./setup` took 31 seconds after the clone on a 4-vCPU Linux cloud machine (v1.91.13.0, clean HOME, including the binary build and the Chromium download). Slower networks and laptops take longer. - -**Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Git](https://git-scm.com/), [Bun](https://bun.sh/) v1.4.2+ (the tested version; setup refuses Bun older than 1.3.3, which would let compiled tools read a project's `.env`, and warns in between), [Node.js](https://nodejs.org/) (Windows only). **Recommended on macOS:** the [Aside](https://aside.com) browser (macOS 15+) — browser skills, `/make-pdf`, and `/diagram` drive it first, with your real logged-in sessions. Without it, `./setup` builds gstack's own bundled browser and the same skills use that. `/cso` additionally needs a Bun release with all four `--no-compile-autoload-*` build flags plus a native toolchain: a static-capable C compiler on Linux, Xcode command-line tools on macOS, or Visual Studio 2022 Build Tools with Desktop development with C++ on Windows. If those are absent, setup installs everything else, removes stale CSO helpers, and `/cso` reports `not assessed` with the prerequisite. - -When qualified CSO runtime images are published, setup gives each automatic preload a 30-second window plus a bounded setup allowance for the declared catalog. For slower registries, set an integer such as `GSTACK_CSO_IMAGE_PULL_TIMEOUT_SECONDS=120` (accepted range: 5–300 seconds). One image timing out does not consume the remaining images' windows; setup reports partial progress and a later run resumes from exact digests already present in local Docker. The complete preload is capped at one hour. +**Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Git](https://git-scm.com/), [Bun](https://bun.sh/) v1.4.2+ (the tested version; setup refuses Bun older than 1.3.3, which would let compiled tools read a project's `.env`), and [Node.js](https://nodejs.org/) on Windows. **Recommended on macOS:** the [Aside](https://aside.com) browser (macOS 15+), which the browser skills, `/make-pdf` and `/diagram` drive first with your real logged-in sessions; without it, `./setup` builds gstack's own bundled browser. `/cso` needs a native toolchain as well; setup says so and leaves it unavailable when the toolchain is absent. Timing, the `/cso` prerequisites and the hooks setup registers: [docs/install.md](docs/install.md). ### Step 1: Install on your machine @@ -62,48 +38,11 @@ From inside your repo, paste this. Switches you to team mode, bootstraps the rep (cd ~/.claude/skills/gstack && ./setup --team) && ~/.claude/skills/gstack/bin/gstack-team-init required && git add .claude/ CLAUDE.md && git commit -m "require gstack for AI-assisted work" ``` -No vendored files in your repo, no version drift, no manual upgrades. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent). +No vendored files in your repo, no version drift, no manual upgrades. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent). Team mode follows the `stable` channel: the weekly tagged release (Monday mornings, plus off-cycle hotfixes), not every merge to main. `gstack-config set update_channel main` follows every merge instead; see [docs/releases.md](docs/releases.md). Swap `required` for `optional` if you'd rather nudge teammates than block them. -`required` blocks skills with a hook that runs on [Node.js](https://nodejs.org/), so every teammate needs Node.js on PATH, on every OS. With Claude Code 2.1.295 or later the hook also blocks when it can't run (no Node.js, or a missing `.claude/hooks/check-gstack.cjs`); older Claude Code lets skills through in that case. - -### OpenClaw - -OpenClaw spawns Claude Code sessions via ACP, so every gstack skill just works -when Claude Code has gstack installed. Paste this to your OpenClaw agent: - -> Install gstack: run `git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup` to install gstack for Claude Code. Then add a "Coding Tasks" section to AGENTS.md that says: when spawning Claude Code sessions for coding work, tell the session to use gstack skills. Include these examples — security audit: "Load gstack. Run /cso", code review: "Load gstack. Run /review", QA test a URL: "Load gstack. Run /qa https://...", build a feature end-to-end: "Load gstack. Run /autoplan, implement the plan, then run /ship", plan before building: "Load gstack. Run /office-hours then /autoplan. Save the plan, don't implement." - -**After setup, just talk to your OpenClaw agent naturally:** - -| You say | What happens | -|---------|-------------| -| "Fix the typo in README" | Simple — Claude Code session, no gstack needed | -| "Run a security audit on this repo" | Spawns Claude Code with `Run /cso` | -| "Build me a notifications feature" | Spawns Claude Code with /autoplan → implement → /ship | -| "Help me plan the v2 API redesign" | Spawns Claude Code with /office-hours → /autoplan, saves plan | - -See [docs/OPENCLAW.md](docs/OPENCLAW.md) for advanced dispatch routing and -the gstack-lite/gstack-full prompt templates. - -### Native OpenClaw Skills (via ClawHub) - -Four methodology skills that work directly in your OpenClaw agent, no Claude Code -session needed. Install from ClawHub: - -``` -clawhub install gstack-openclaw-office-hours gstack-openclaw-ceo-review gstack-openclaw-investigate gstack-openclaw-retro -``` - -| Skill | What it does | -|-------|-------------| -| `gstack-openclaw-office-hours` | Product interrogation with 6 forcing questions | -| `gstack-openclaw-ceo-review` | Strategic challenge with 4 scope modes | -| `gstack-openclaw-investigate` | Root cause debugging methodology | -| `gstack-openclaw-retro` | Weekly engineering retrospective | - -These are conversational skills. Your OpenClaw agent runs them directly via chat. +Team mode's `required` hook runs on Node.js, so every teammate needs Node.js on PATH ([details](docs/install.md#team-mode)). To remove gstack later: [docs/uninstall.md](docs/uninstall.md). ### Other AI Agents @@ -121,16 +60,6 @@ An explicit `--host X` installs for X only and never changes another agent's install. `/gstack-upgrade` refreshes every install it registered, one row per host, and says which ones failed. -A checkout carries skills only for the agents installed from it. Setup records -them in `.gstack-installed-hosts` and never drops one, so adding an agent later -is just `./setup --host `. Copies rendered for other agents are pruned: -generated files move to `~/.gstack/backups/host-renders/`, and files gstack -can't prove it generated stay where they are. A global Claude install went -from 632 `SKILL.md` files (34.7 MB) to its own 63 (2.5 MB), which keeps Cursor-agent -from freezing on the skills tree (#1694). In a development checkout, -`bun run build` still renders every agent; inside an install it renders the -recorded ones, and `GSTACK_RENDER_HOSTS=all bun run build` renders them all. - Tiers: **full** is certified by a real workflow run (see [Certify your host](docs/ADDING_A_HOST.md#certify-your-host)); **experimental** installs and passes the conformance tests but has no certification run yet; @@ -151,85 +80,7 @@ installs and passes the conformance tests but has no certification run yet; | Hermes | instruction-only | `--host hermes` (prints the digest path and `gen:skill-docs --host hermes`) | copy the digest, or render skills yourself | advisory, not blocked | — | re-copy the digest after upgrades | | GBrain (mod) | instruction-only | `--host gbrain` (brain-aware variants ship from the GBrain repo) | — | advisory, not blocked | — | — | -Copilot invokes gstack skills by their prefixed names (`/gstack-review`) because -`/review` is a Copilot built-in. Copilot ignores skill hooks, so `/careful` and -`/freeze` only advise there, and `COPILOT_HOME` other than `~/.copilot` is not -supported yet (setup refuses and changes nothing). - -Antigravity CLI also invokes skills by prefixed name (`/gstack-review`). Setup -writes only `~/.gemini/antigravity-cli/skills/gstack*`: Gemini CLI's -`~/.gemini/skills` and settings are never touched, and `--host auto` selects -Antigravity only when `agy` is on `PATH` or `~/.gemini/antigravity-cli` exists, -never for a Gemini-CLI-only `~/.gemini`. The Antigravity 2.0 app and IDE read -`~/.gemini/config/skills`, which this host does not install. - -Outside reviews require the selected CLI to be installed and authenticated: Claude Code when using gstack in Codex, or Codex on other harnesses. External harnesses discover these commands as `/gstack-claude-code` and `/gstack-codex`; each harness omits its own wrapper. Explicit provider requests keep that provider. The existing `codex_reviews` setting controls automatic outside reviews where supported, regardless of the provider selected. - -`/claude` has been renamed to `/claude-code`. Re-run `./setup --host ` to migrate managed installations, including other harnesses sharing the checkout. Setup preserves the previous installation if replacement generation or installation fails and prints repair instructions. - -**Instruction-only tier (any rules-reading agent — Zed, Amp, Jules, side projects):** -copy the 2KB digest at [`agents-digest/gstack-AGENTS.md`](agents-digest/gstack-AGENTS.md) -into a location your agent reads (for example, append it to your project's `AGENTS.md`). -It carries gstack's ethos, reuse ladder, and voice rules — no install required. The -digest's first line shows its gstack version; re-copy it after upgrading. - -For Codex, setup reads the top-level `model` from -`${CODEX_HOME:-~/.codex}/config.toml` and generates the matching behavioral -profile, falling back to `gpt-6-astra` when no usable model is configured. -`gpt-5.6-sol` automatically receives bounded-scope instructions that -finish the requested lake without expanding into adjacent cleanup or speculative -hardening. The Sol profile is exact-match only: dated snapshots and other 5.6 -variants get the generic GPT profile, and setup warns on near-misses like -`gpt-5.6-sol-2026-08-01`. Override detection with `./setup --host codex --model ` — the -override applies to that run only; set `model` in your Codex `config.toml` to -make it stick across upgrades. After changing your Codex model, rerun -`./setup --host codex` to regenerate the skills. - -**Plan-review and implementation tiers.** Plan workflows use an independent -`frontier` reviewer: Fable 5.1 from Codex, or GPT-6 Astra from Claude Code. -Implementation handoffs recommend the `smart` tier: Opus 5.5 or GPT-6.1 Sol. -Choose a different tier, pin either provider's model, or use `host` mode to -preserve native model settings. The read-only `gstack-models` command explains -the effective choices; changing these settings does not require regenerating -skills or switch your running session. A weekly maintenance check flags changed -official recommendations and retirement notices without automatically changing -models. See [model policy: setup, overrides and freshness](docs/model-policy.md). - -**Other Codex calls keep their existing selection.** Without the explicit -`plan-review` role (including ordinary review and ship adversarial passes), gstack picks the model in this -order: a model you name for that request, then `GSTACK_CODEX_MODEL`, then -`model` in your Codex `config.toml` (for native `codex review`, `review_model` -first; a custom `CODEX_HOME` is honored), and only then gstack's default, -`gpt-6-astra`. Before spending anything, it prints the choice and where it came -from, for example `CODEX_MODEL: gpt-5.6-terra (exec; source: ~/.codex/config.toml model)`. -If your choice is invalid or your account cannot use it, gstack stops with a -repair message and reports the outside review as unavailable. It never silently -switches to its default. Nested Codex reviews also run with installed skills -hidden (`-c skills.include_instructions=false`), so a review cannot turn into a -whole nested skill run. Runtime model selection is separate from the setup-time -behavioral profile above. Without the plan-review role, `/claude-code` (`gstack-claude-code` -on Codex) preserves Claude's configured model. Set `GSTACK_CLAUDE_MODEL=` -or name a model in your request to override it for the invocation, including -resumed consultations. See [eval defaults and overrides](CONTRIBUTING.md#testing--evals) -for capture, judge, and benchmark model selection. - -The design binary (`$D`) uses OpenAI's `gpt-5.5` by default, with image -generation through `gpt-image-2`. Set `GSTACK_DESIGN_MODEL=` to use -another gpt-5-class model for both image generation and screenshot analysis; -if OpenAI rejects it, the error names `GSTACK_DESIGN_MODEL`. Set -`GSTACK_DESIGN_IMAGE_MODEL=` to change the image tool model; a -value that is not a gpt-image model name is refused before any request. Check a -key against the defaults with `bun run design/scripts/live-model-check.ts`, -which always tests the default models and ignores both overrides; the weekly -periodic census runs the same check. Set `OPENAI_BASE_URL` to send every `$D` -call to an OpenAI-compatible gateway instead of `api.openai.com`; egress -receipts record the gateway host. On Codex, `/design-shotgun` generates mockups -with Codex's built-in `$imagegen` skill instead, so it needs no -`OPENAI_API_KEY`; `$D` still builds and serves the comparison board. - -**Want to add support for another agent?** See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md). -Rendering a new agent is one TypeScript config file; installing it also needs a -setup arm and the conformance kit, and it starts as experimental. +Per-host notes (Copilot, Antigravity, OpenClaw and ClawHub, the Codex behavioral profile and model tiers, outside-review providers, the design binary's models) are in [docs/hosts.md](docs/hosts.md). Any rules-reading agent (Zed, Amp, Jules) can use the 2KB [agents digest](agents-digest/gstack-AGENTS.md) with no install. Adding a host: [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md). ## See it work @@ -319,50 +170,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan- | `/make-pdf` | **Publisher** | Markdown in, publication-quality document out. Mermaid and excalidraw fences render as vector diagrams, fully offline. Images scale to the page and never truncate; wide diagrams get their own landscape page. `--to html` emits one self-contained file, `--to docx` a Word doc. | | `/diagram` | **Diagram Maker** | English in, editable diagram out. Emits a triplet: mermaid source, `.excalidraw` you can open and edit on excalidraw.com (hand-drawn style), and rendered SVG/PNG. Zero network. Embed the source in markdown and `/make-pdf` renders it. | -### QA without a webpage - -Use the same commands for browser and non-browser software. Start in a repository -with its documented native command and an isolated local fixture; name the target -and the behavior you want checked. For example: - -```text -/qa-only Test this repo's CLI using its documented local fixture. Check valid and invalid input, exit codes, stdout/stderr, and cancellation. Report only; do not change code or tests. For each finding, include the exact command, expected and actual results, and which checks remain untested. Keep requests inside the fixture; ask before contacting an external service. - -/qa Test this repo's local webhook and worker fixture. Explore duplicate deliveries and recovery after a partial failure. Keep all effects inside the fixture; preserve reproduced bugs in native regression tests before repairing them. -``` - -QA first tells you which surface, tools and write permissions it will use. A CLI or -API does not need a browser. Browser targets keep real browser testing; developer -experience audits load only when onboarding, installation or ergonomics are in scope. -If the native tools or safe fixture are unavailable, the report names the blocker -and untested contracts instead of inventing a pass or installing another framework. - -Exploration means learning from each result and choosing the next useful challenge, -not running random commands. Before the next discovery probe, QA saves a short -`exploration-NNN.json` evidence note with the previous result, the assumption being -tested and the next command. The final report links these notes; they do not require -an extra chat message between probes. A discovered bug must be reproducible, and its new test -must fail for the bug before the repair and pass afterward. Unit tests protect logic; -integration and end-to-end tests protect real boundaries that mocks would hide. -`/qa-only` proposes those tests without writing them. - -Normal `/review` and `/ship` run a bounded version on changed behavior and nearby -risks automatically, including small diffs without a plan or web server. Existing -fix/test approval rules still apply. Missing dependencies, denied actions and time -limits remain visible coverage gaps; a short smoke pass never means exhaustive QA. -Production access and destructive or external effects require specific permission. - -Bounded exploration uses an executable deadline guard, not an estimated clock: it -refuses late probes and stops owned foreground work at the limit. Unfinished checks -stay visible in the report. Required plan checks remain outside the review/ship smoke -budget. See [QA deadlines](docs/reference-qa-deadlines.md) for command, platform and -cleanup limits. - -Every ship also runs the existing documentation audit, including repeat ships and -existing PR updates. Clear factual corrections join the final checked change; risky -rewrites need approval. A failed audit stops for recovery or explicit acceptance of -the named risk rather than silently dropping its result. Ship owns versioning, Git -and PR publication; the docs helper does not commit or push independently. +The same `/qa` and `/qa-only` commands test CLIs, APIs, jobs, workers and webhooks with no browser: [QA without a webpage](docs/qa-without-a-webpage.md). ### Which review should I use? @@ -391,237 +199,10 @@ and PR publication; the docs helper does not commit or push independently. | `/ios-qa` | **iOS Live-Device QA (v1.43.0.0+)** — drive a real iPhone or iPad over USB CoreDevice via an embedded `StateServer` in the app. Read Swift source, codegen typed `@Observable` accessors, run the agent loop. Optional `--tailnet` flag exposes the device to OpenClaw or any HTTP-capable agent on your Tailscale tailnet so remote agents can run iOS QA without ever touching the hardware. Capability-tier allowlist (observe/interact/mutate/restore), per-device session lock, audit log. | | `/ios-fix`, `/ios-design-review`, `/ios-clean`, `/ios-sync` | iOS bug-fix loop, designer's-eye HIG audit, debug-bridge cleanup, and accessor resync. See `docs/skills.md`. End-to-end walkthrough: [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-testing-with-gstack.md). | -### Standalone binaries - -Beyond the slash-command skills, gstack ships standalone CLIs for workflows that don't belong inside a session: - -| Command | What it does | -|---------|-------------| -| `gstack-model-benchmark` | **Cross-model benchmark** — run the same prompt through Claude, GPT (via Codex CLI), and Gemini; compare latency, tokens, cost, and (optionally) LLM-judge quality score. Auth detected per provider, unavailable providers skip cleanly. Output as table, JSON, or markdown. `--dry-run` validates flags + auth without spending API calls. | -| `gstack-taste-update` | **Design taste learning** — writes approvals and rejections from `/design-shotgun` into a persistent per-project taste profile. Decays 5%/week. Feeds back into future variant generation so the system learns what you actually pick. | -| `gstack-egress` | **Egress receipt auditor** — every gstack-initiated off-machine send writes a tamper-evident, hash-chained receipt to `~/.gstack/security/egress.jsonl` before the send. `list` shows what gstack attempted to send and to which host, `grants` shows the standing consent settings plus the exact command that revokes each, `verify` recomputes the hash chain and exits 3 on tamper (catches edits, reordering, and mid-chain deletion; truncating or deleting the ledger itself is out of scope — it's a forensic log, not tamper-proof storage). | -| `gstack-context-bill` | **Token bill-of-materials** — read-only, offline audit of what an installed skills tree costs in tokens: always-on frontmatter every session pays vs per-invocation SKILL.md + forced references. `--diff` compares two trees, `--budget` enforces a ceiling, `--exact` opts into Anthropic `count_tokens` (sends file text off-machine; writes an egress receipt first, degrades to the offline estimate if the receipt can't be written). | -| `gstack-code-intelligence` | **Code-intelligence provider picker** — wraps GBrain, Sourcebot, and Graphify behind one interface: `options`/`status` to see what's available, `select` to pick one, `index`/`search` to use it, `suggest` to check whether the one-time indexing offer should fire here. The offer triggers on large repos (1,000+ tracked files; a decline is persisted). Non-local providers refuse to index *or search* until you record per-repo consent (`consent yes\|no` — the query text is repo-derived content), the per-repo trust policy's deny and read-only tiers veto write-class operations regardless of consent, and every off-machine send writes an egress receipt. Fully optional — with nothing selected, gstack falls back to grep. | -| `gstack-verify-gate` | **Verification stop hook (opt-in)** — blocks a Claude Code turn from ending until the project's declared verify command passes (after 3 blocked re-entries it yields with a loud still-RED warning instead of looping forever). Declare it on one line in CLAUDE.md: ``. Hooks bypass the permission system, so a declared command never runs until you trust it once per repo (`gstack-verify-gate --trust`); editing the command invalidates trust until re-granted, and every grant is audit-logged. `./setup` never registers it for you — opt in with `gstack-settings-hook add-event --event Stop --command ~/.claude/skills/gstack/bin/gstack-verify-gate --source verify-gate`, remove with `gstack-settings-hook remove-source --source verify-gate`. | -| `gstack-memorable` | **Memorable recall bridge (opt-in, third party, Claude Code only)** — connects Claude Code to the external [Memorable](https://memorable.sh) CLI *through gstack* instead of the vendor's own installer, so the hook gets gstack's guarantees: an explicit consent key (`memorable_recall`, off by default, listed by `gstack-egress grants`), a fail-closed egress receipt for every prompt handed over (`gstack-egress list --sink memorable-recall`), a HIGH-tier secret pre-scan, a trust envelope and 8 KiB cap on whatever comes back, an allowlisted environment and process-group containment for the vendor process, and clean removal. `enable` registers the hook at the stable install with a 5 s timeout and never runs the vendor's own consent command; `disable` revokes the gate first and removes the entry by identity even after Claude Code strips the tag; `status` is read-only. gstack never installs Memorable, and what its binary sends is the vendor's claim, not gstack's. Not available on Windows yet. [Full guide](docs/memorable-workflow-memory.md). | -| `gstack-wtree` | **Working-tree fingerprint** — prints a content hash of what's actually on disk (temp index seeded from the stat cache, ~40x cheaper than a full re-hash; untracked source counts, gitignored scratch doesn't). Identical content fingerprints identically through commits, rebases, amends, and squashes — it's what binds reviews and test evidence to content instead of commit SHAs. | -| `gstack-review-log` | **Review-pass receipts** — `--start ` captures the working-tree fingerprint before a diff review; `'' --finish ` consumes that single-use, repository/branch/skill-scoped receipt. Binding requires matching start/end content and reviewer-reported `completed:true` and `converged:true`; it is not independent proof that a model read the source. `--check-shared-libs ` reads a current finding as JSON on stdin and checks prior Skip decisions against the actual branch, capture and eligible raw source blobs. It returns `reusable:false` when proof is missing or unsafe, including older records without logger-versioned coverage. Final review logging computes shared-code fingerprints and coverage rather than trusting supplied proof. | -| `gstack-review-read` | **Review freshness** — emits review records with computed `review_freshness.status` and `reason`: CURRENT, STALE, or UNVERIFIED for diff reviews. `/ship` and `/land-and-deploy` use the same grade; a matching commit alone never certifies a diff review. [Dashboard rules](docs/skills.md#review-readiness-dashboard). | -| `gstack-evidence` | **Verification-evidence ledger** — `run --label -- ` transparently wraps any test command (the child's exit code always passes through) and records what ran against which working-tree fingerprint; `check` grades each label FRESH/STALE/MISSING with `--expect-cmd`, `--max-age`, and `--allow-paths` binding. /ship and /land-and-deploy cite fresh evidence instead of re-running suites. Per-run logs are 0600, capped at 2MB, pruned after 30 days; the ledger and logs stay machine-local by design. | -| `gstack-issue-guard` | **Tracker-text trust envelope** — fetches GitHub issue/PR text (`issue `, `pr-body`, `pr-comments`, or `--stdin`) and wraps it in a labeled envelope so agents treat it as data: injection-shaped lines get labeled even through fullwidth and invisible-character evasion, and forged envelope banners are defused. Every tracker-text ingress in gstack routes through it, enforced by a CI scanner. | -| `gstack-ios-qa-daemon` | **iOS QA daemon** — Mac-side broker between an agent and a connected iPhone or iPad over USB CoreDevice. Loopback by default; `--tailnet` opens a Tailscale-facing listener with identity-gated capability tiers. Single-instance via flock on `~/.gstack/ios-qa-daemon.pid`. See [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-testing-with-gstack.md). | -| `gstack-ios-qa-mint` | **iOS allowlist manager** — owner-grant CLI for the tailnet allowlist. `grant`/`revoke`/`list` against `~/.gstack/ios-qa-allowlist.json` (mode 0600). Remote agents never auto-allowlist; this is the explicit-intent path. | -| `gstack-ios-qa-regen` | **iOS bridge regenerator** — deterministically installs the canonical DebugBridge package, generates typed state accessors, and records the installed gstack version. Safe to rerun after source changes or upgrades. | - -The private paid CSO evaluation producer is a packaging contract, not an ordinary `bun run build` artifact. On macOS or Linux, a release operator compiles `cso-eval-producer` with the documented hardened Bun flags in the same clean build session as `bun run build:cso`, then installs it beside `gstack-cso-launcher`, `gstack-cso-core`, `gstack-cso-watchdog`, and the hidden `.gstack-cso-generation` manifest as one root-owned, nonwritable five-artifact unit. The producer rejects root execution, writable/symlinked/incomplete installations, and unreviewed provider CLI versions; each receipt binds all five artifact hashes, and collection rejects receipts from different unit identities. See the [clean producer procedure](test/fixtures/cso-eval/README.md#trusted-five-artifact-producer-unit). Paid producer evaluation remains unavailable on Windows because the detached watchdog has no Windows build. - -Each producer gets a curated one-cell source copy with directories sealed to `0555` and files to `0444`, plus pre/post content, Git, and mode checks. Claude receives no source add-directory, because an add-directory is a sandbox write root where Claude Code's bubblewrap leaves an empty `.mcp.json` mount point. Its Bash runs inside Claude Code's sandbox with one exception: the exact absolute launcher path that `--allowed-tools` names, which runs outside it as it does for users. The launcher is the boundary there. It replaces its environment with a fixed allowlist, uses only the local Docker Unix socket, and runs every target in hardened containers. The producer passes that policy through `--settings` (sandbox enabled, fail if unavailable, no unsandboxed fallback, no auto-allow, and `excludedCommands` naming only the launcher). It refuses to start the agent (`CLAUDE_PRODUCER_SANDBOX_POLICY_MISMATCH`) unless the generated arguments and environment carry exactly that policy. `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` stays unset, because it would force the launcher back into the sandbox. Every other Bash command is denied by `--allowed-tools`, and Write is confined to the provider work directory and `cso-home`. Gemini receives no source working directory or include-directory. Codex technically receives read-only filesystem access to the exact curated source root because the trusted helper inherits the Codex permission profile; its private provider work directory and exact `cso-home` artifact directory are the only write roots, every other root path remains denied, and the producer prompt requires source access through the helper. This is evaluation containment for an immutable public fixture, not a claim that Codex cannot directly read that fixture. - -Copy each completed cell's receipt together with `state/cso-home/security/cso/` to the trusted adjudication host. Receipt entries are sorted paths relative to that directory and bind every retained file's size and SHA-256 plus an aggregate inventory hash. Provider homes, settings, sessions, and credentials live under separate disposable directories and are removed after the cell; they are never part of the retained artifact tree. The adjudicator must re-hash the transported tree against the receipt before trusting reports or repair bundles. - -Paid producer qualification uses this explicit host matrix: - -| Producer host | Daily/static cells | Comprehensive target execution | -|---|---|---| -| Codex CLI 0.153.4 | Supported under the custom permission profile | Fails closed because Docker/socket access is not granted to the model command sandbox; report the setup gap as partial | -| Claude Code 2.1.263 | Supported under restricted safe mode | Fails closed when the restricted launcher child cannot reach Docker; report the setup gap as partial | -| Gemini CLI 0.59.0 | Supported with isolated home/settings | Private release qualification host: the exact `run_shell_command()` policy can run the trusted helper without exposing a general shell | - -Every setup-blocked comprehensive cell remains a miss in release-gate denominators. The evaluator does not silently count a host or workflow as supported when its containment policy prevents required setup. - -`./setup` also registers one default-on Stop hook in `~/.claude/settings.json`: -`gstack-timeline-stop` (closes dangling session-timeline entries when a session -is interrupted; fail-open — 2s internal budget, always exits 0, can never block -a session). Opt out persistently with `./setup --no-timeline-stop-hook` — the -choice lands in the `timeline_stop_hook` config key, survives upgrades, and an -explicit "no" removes a live registration. `GSTACK_TIMELINE_STOP_HOOK=no` and -`gstack-config set timeline_stop_hook no` work too (flag > env > config). -`./setup --no-team` skips it for that run, `gstack-settings-hook remove-source ---source gstack-timeline-stop` removes it by hand, and `gstack-uninstall` -removes it too. - -Hook registration is canonical-only: every hook command points at the stable -`~/.claude/skills/gstack` install, never the tree setup ran from, so deleting -a worktree or Conductor workspace can't leave dead hooks erroring in your -sessions. Every `./setup` run also heals first: `gstack-settings-hook -prune-stale --repoint` removes dead gstack hook entries, re-points stale ones -at the stable install, and collapses duplicates, printing one line (and -writing a backup beside the file) only when it changed something. - -### Domain skills + raw CDP escape hatch - -Two browser primitives in gstack's own engine (the fallback path when Aside isn't there) compound the agent over time: - -- **`$B domain-skill save`** — agent saves a per-site note (e.g., "LinkedIn's Apply button lives in an iframe") that fires automatically next time it visits that hostname. Quarantined → active after 3 successful uses → optional cross-project promotion via `$B domain-skill promote-to-global`. Storage lives alongside `/learn`'s per-project learnings file. Full reference: **[docs/domain-skills.md](docs/domain-skills.md)**. -- **`$B cdp `** — raw Chrome DevTools Protocol escape hatch for the rare case curated commands miss. Deny-default: methods must be explicitly added to `browse/src/cdp-allowlist.ts` with a one-line justification. Two-tier mutex serializes browser-scoped CDP calls against per-tab work. Output for data-exfil methods is wrapped in the UNTRUSTED envelope. - -> Want raw CDP with no rails, no allowlist, no daemon — just thin transport from agent to Chrome? [browser-use/browser-harness-js](https://github.com/browser-use/browser-harness-js) is a different philosophy (agent-authored helpers vs gstack's curated commands) and a good fit if you don't want gstack's security stack. The two can coexist: gstack's `$B cdp` and harness can both attach to the same Chrome via Playwright's `newCDPSession`. +Beyond the slash commands, gstack ships standalone CLIs (`gstack-egress`, `gstack-context-bill`, `gstack-model-benchmark`, the review and evidence ledgers, the iOS QA daemon): [docs/standalone-binaries.md](docs/standalone-binaries.md). The fallback browser's per-site [domain skills](docs/domain-skills.md) and raw CDP escape hatch, and the [voice-input aliases](docs/voice-input.md) many skills carry, have their own pages. **[Deep dives with examples and philosophy for every skill →](docs/skills.md)** -### Karpathy's four failure modes? Already covered. - -Andrej Karpathy's [AI coding rules](https://github.com/forrestchang/andrej-karpathy-skills) (17K stars) nail four failure modes: wrong assumptions, overcomplexity, orthogonal edits, imperative over declarative. gstack's workflow skills enforce all four. `/office-hours` forces assumptions into the open before code is written. The Confusion Protocol stops Claude from guessing on architectural decisions. `/review` catches unnecessary complexity and drive-by edits. `/ship` transforms tasks into verifiable goals with test-first execution. If you already use Karpathy-style CLAUDE.md rules, gstack is the workflow enforcement layer that makes them stick across entire sprints, not just single prompts. - -## Parallel sprints - -gstack works well with one sprint. It gets interesting with ten running at once. - -**Design is at the heart.** `/design-consultation` builds your design system from scratch, researches what's out there, proposes creative risks, and writes `DESIGN.md`. But the real magic is the shotgun-to-HTML pipeline. - -**`/design-shotgun` is how you explore.** You describe what you want. It generates 4-6 AI mockup variants using GPT Image. Then it opens a comparison board in your browser with all variants side by side. You pick favorites, leave feedback ("more whitespace", "bolder headline", "lose the gradient"), and it generates a new round. Repeat until you love something. Taste memory kicks in after a few rounds so it starts biasing toward what you actually like. No more describing your vision in words and hoping the AI gets it. You see options, pick the good ones, and iterate visually. - -**Works with impeccable.** If you use [impeccable](https://impeccable.style) too, gstack does not fight it. gstack runs impeccable's deterministic engine as a pre-pass in `/design-review`, `/review`, `/ship`, and `/design-html` when you have it installed (gstack never runs impeccable's installer or launcher; the first time a design skill finds no engine it asks once whether to download the engine binary, checksum-pinned and logged, into `~/.impeccable`, and remembers your answer), speaks the same 61 rule ids in its own voice, reads `PRODUCT.md`, writes `DESIGN.md` in the open DESIGN.md format both tools read, and hands deferred findings to `/impeccable `. Say no and nothing changes: no nag, no missing step. `gstack-config set design_detector off` turns the pre-pass off. Attribution for the material gstack derived from impeccable and the DESIGN.md spec is in `NOTICE.md`. - -**`/design-html` makes it real.** Take that approved mockup (from `/design-shotgun`, a CEO plan, a design review, or just a description) and turn it into production-quality HTML/CSS. Not the kind of AI HTML that looks fine at one viewport width and breaks everywhere else. This uses Pretext for computed text layout: text actually reflows on resize, heights adjust to content, layouts are dynamic. 30KB overhead, zero dependencies. It detects your framework (React, Svelte, Vue) and outputs the right format. Smart API routing picks different Pretext patterns depending on whether it's a landing page, dashboard, form, or card layout. The output is something you'd actually ship, not a demo. - -**`/qa` was a massive unlock.** It let me go from 6 to 12 parallel workers. Claude Code saying *"I SEE THE ISSUE"* and then actually fixing it, generating a regression test, and verifying the fix — that changed how I work. The agent has eyes now. - -**Smart review routing.** Just like at a well-run startup: CEO doesn't have to look at infra bug fixes, design review isn't needed for backend changes. gstack tracks what reviews are run, figures out what's appropriate, and just does the smart thing. The Review Readiness Dashboard tells you where you stand before you ship. - -**Test everything.** `/ship` bootstraps test frameworks from scratch if your project doesn't have one. Every `/ship` run produces a coverage audit. `/qa` creates native regressions when infrastructure is available and explicitly reports missing test coverage; CSS-only fixes may use browser evidence instead. 100% test coverage is the goal — tests make vibe coding safe instead of yolo coding. - -**`/document-release` is the engineer you never had.** It audits relevant authored docs against the release diff and corrects factual drift before final verification. Risky changes return for approval. `/ship` invokes it on every run and owns TODOS, release metadata, generation and publication; the child reports updated, current or blocked documentation. - -**Aside is the browser gstack drives first.** For browser surfaces, on a Mac with the [Aside](https://aside.com) AI browser open, `/qa`, `/qa-only`, `/design-review`, `/canary`, `/benchmark`, `/scrape`, and `/browse` all run there — your real browser, with your real logged-in sessions, in tabs the agent opens for itself and closes when it's done. No cookie import, no "open the browser" step, no CAPTCHA handoff dance: hit a sign-in wall, sign in inside Aside, say "done", and the agent continues. Anything a page returns is treated as untrusted content — the agent takes syntax from it, never instructions. `/make-pdf`, `/diagram`, and design previews print and screenshot through Aside too (served from your machine on loopback, one render per script), and the planning skills do their web research through Aside's own agent before reaching for a search tool. - -**When Aside isn't there, gstack's own browser takes over — automatically.** Linux, Windows, or a Mac with Aside closed: the same skills use the bundled headless Chromium that `./setup` builds, produce the same evidence, and light up the features below that only make sense when the browser is gstack's rather than yours. - -**Real browser mode.** `/open-gstack-browser` launches GStack Browser, an AI-controlled Chromium with anti-bot stealth, custom branding, and the sidebar extension baked in. Sites like Google and NYTimes work without captchas. The menu bar says "GStack Browser" instead of "Chrome for Testing." Your regular Chrome stays untouched. All existing browse commands work unchanged. `$B disconnect` returns to headless. The browser stays alive as long as the window is open... no idle timeout killing it while you're working. - -**Sidebar agent — your AI browser assistant.** Type natural language in the Chrome side panel and a child Claude instance executes it. "Navigate to the settings page and screenshot it." "Fill out this form with test data." "Go through every item in this list and extract the prices." The sidebar auto-routes to the right model: Sonnet for fast actions (click, navigate, screenshot) and Opus for reading and analysis. Each task gets up to 5 minutes. The sidebar agent runs in an isolated session, so it won't interfere with your main Claude Code window. One-click cookie import right from the sidebar footer. - -**Personal automation.** The sidebar agent isn't just for dev workflows. Example: "Browse my kid's school parent portal and add all the other parents' names, phone numbers, and photos to my Google Contacts." Two ways to get authenticated: (1) log in once in the headed browser, your session persists, or (2) click the "cookies" button in the sidebar footer to import cookies from your real Chrome. Once authenticated, Claude navigates the directory, extracts the data, and creates the contacts. - -**Prompt injection defense.** Hostile web pages try to hijack your sidebar agent. gstack ships a layered defense: content filters (datamarking, hidden-element stripping, ARIA scrubbing, URL blocklist) on every page read, plus a 22MB ML classifier running locally in a sidecar subprocess that scans page-derived content before the agent sees it, with a verdict combiner that requires classifier agreement before blocking (prevents single-model false positives on Stack Overflow-style instruction pages). Everything runs on your machine, no network calls. Emergency kill switch: `GSTACK_SECURITY_OFF=1`. See [ARCHITECTURE.md](ARCHITECTURE.md#prompt-injection-defense-sidebar-agent) for the full stack. - -**Browser handoff when the AI gets stuck.** Hit a CAPTCHA, auth wall, or MFA prompt? `$B handoff` opens a visible Chrome at the exact same page with all your cookies and tabs intact. Solve the problem, tell Claude you're done, `$B resume` picks up right where it left off. The agent even suggests it automatically after 3 consecutive failures. - -**`/pair-agent` is cross-agent coordination.** You're in Claude Code. You also have OpenClaw running. Or Hermes. Or Codex. You want them both looking at the same website. Type `/pair-agent`, pick your agent, and a GStack Browser window opens so you can watch. The skill prints a block of instructions. Paste that block into the other agent's chat. It exchanges a one-time setup key for a session token, creates its own tab, and starts browsing. You see both agents working in the same browser, each in their own tab, neither able to interfere with the other. If ngrok is installed, the tunnel starts automatically so the other agent can be on a completely different machine. Same-machine agents get a zero-friction shortcut that writes credentials directly. This is the first time AI agents from different vendors can coordinate through a shared browser with real security: scoped tokens, tab isolation, rate limiting, domain restrictions, and activity attribution. - -**Multi-AI second opinion.** In Codex, gstack sends outside reviews to Claude Code through `/claude-code`. In Claude Code, `/codex` sends them to OpenAI Codex. Other harnesses expose both skills and use Codex where automatic outside reviews are supported. Each skill supports code review, adversarial challenge, and consultation with session continuity. Routing follows the harness, so changing your configured model does not change the outside reviewer. Reports identify the provider that actually completed each review; unavailable outside coverage remains visible. - -**Safety guardrails on demand.** Say "be careful" and `/careful` warns before any destructive command — rm -rf, DROP TABLE, force-push, git reset --hard. `/freeze` locks edits to one directory while debugging so Claude can't accidentally "fix" unrelated code. `/guard` activates both. `/investigate` auto-freezes to the module being investigated. - -**Proactive skill suggestions.** gstack notices what stage you're in — brainstorming, reviewing, debugging, testing — and suggests the right skill. Don't like it? Say "stop suggesting" and it remembers across sessions. - -## 10-15 parallel sprints - -gstack is powerful with one sprint. It is transformative with ten running at once. - -[Conductor](https://conductor.build) runs multiple Claude Code sessions in parallel — each in its own isolated workspace. One session running `/office-hours` on a new idea, another doing `/review` on a PR, a third implementing a feature, a fourth running `/qa` on staging, and six more on other branches. All at the same time. I regularly run 10-15 parallel sprints — that's the practical max right now. - -The sprint structure is what makes parallelism work. Without a process, ten agents is ten sources of chaos. With a process — think, plan, build, review, test, ship — each agent knows exactly what to do and when to stop. You manage them the way a CEO manages a team: check in on the decisions that matter, let the rest run. - -### Voice input (AquaVoice, Whisper, etc.) - -Many gstack skills list speech-to-text aliases (`voice-triggers:` in their -templates): "security check" for `/cso`, "run QA" or "test the app" for `/qa`, -"tech review" for `/plan-eng-review`. Say one of those naturally and the right -skill can be picked without the slash command name. On Claude Code the aliases -are routing lines in the root `/gstack` router; on Codex and the other hosts -they are appended to each skill's description, which is what those hosts match -on. - -The separate `triggers:` list in a skill's frontmatter is gstack metadata, not a -matching surface. Claude Code and Codex choose a skill from its name and -description, so a phrase that appears only under `triggers:` does not activate -the skill (Codex output drops the field; the GBrain host keeps it). - -## Uninstall - -### Option 1: Run the uninstall script - -If gstack is installed on your machine: - -```bash -~/.claude/skills/gstack/bin/gstack-uninstall -``` - -This handles skills, symlinks, global state (`~/.gstack/`), project-local state, browse daemons, and temp files. Use `--keep-state` to preserve config and analytics. Use `--force` to skip confirmation. - -### Option 2: Manual removal (no local repo) - -If you don't have the repo cloned (e.g. you installed via a Claude Code paste and later deleted the clone): - -```bash -# 1. Stop browse daemons -pkill -f "gstack.*browse" 2>/dev/null || true - -# 2. Remove per-skill directories whose SKILL.md points into gstack/ -# (rm -rf, not rmdir — installed dirs also contain runtime-asset links) -find ~/.claude/skills -mindepth 1 -maxdepth 1 -type d ! -name gstack 2>/dev/null | -while IFS= read -r dir; do - link="$dir/SKILL.md" - [ -L "$link" ] || continue - target=$(readlink "$link" 2>/dev/null) || continue - case "$target" in - gstack/*|*/gstack/*) - rm -rf "$dir" - ;; - esac -done -# Directories gstack created carry a .gstack-owned marker (the only signal on -# Windows, where installs are file copies with no symlink to read) -for marker in ~/.claude/skills/*/.gstack-owned; do - [ -f "$marker" ] && rm -rf "$(dirname "$marker")" -done -# Alias skills install as copies (no symlink to detect) — remove by name -rm -rf ~/.claude/skills/_gstack-command ~/.claude/skills/connect-chrome 2>/dev/null - -# 3. Remove gstack -rm -rf ~/.claude/skills/gstack - -# 4. Remove global state -rm -rf ~/.gstack - -# 5. Remove integrations (skip any you never installed) -rm -rf "${CODEX_HOME:-$HOME/.codex}/skills/gstack"* 2>/dev/null -rm -rf ~/.factory/skills/gstack* 2>/dev/null -rm -rf ~/.kiro/skills/gstack* 2>/dev/null -rm -rf ~/.openclaw/skills/gstack* 2>/dev/null -rm -rf ~/.cursor/skills/gstack* 2>/dev/null -rm -rf ~/.config/opencode/skills/gstack* 2>/dev/null -rm -rf ~/.gemini/antigravity-cli/skills/gstack* 2>/dev/null - -# 6. Remove temp files -rm -f /tmp/gstack-* 2>/dev/null - -# 7. Per-project cleanup (run from each project root) -rm -rf .gstack .gstack-worktrees .claude/skills/gstack 2>/dev/null -rm -rf .agents/skills/gstack* .factory/skills/gstack* 2>/dev/null -``` - -Manual removal leaves gstack's hook entries behind in `~/.claude/settings.json` -(the uninstall script removes all of them for you, including entries whose -`_gstack_source` tag was stripped). Edit that file and delete every hook whose -command path points into `.claude/skills/gstack/`: the SessionStart auto-update -hook, the AskUserQuestion PreToolUse/PostToolUse hooks, and the Stop hooks -(session timeline, plus verify-gate if you opted in). Left in place, they error -on every matching event once the install directory is gone. - -### Clean up CLAUDE.md - -The uninstall script does not edit CLAUDE.md. In each project where gstack was added, remove the `## gstack` and `## Skill routing` sections. - -### Playwright - -`~/Library/Caches/ms-playwright/` (macOS) is left in place because other tools may share it. Remove it if nothing else needs it. - -### Aside - -gstack never installed Aside, so it never uninstalls it. Keep it or remove it like any other app. - ---- - -Free, MIT licensed, open source. No premium tier, no waitlist. - -I open sourced how I build software. You can fork it and make it your own. - -> **We're hiring.** Want to ship real products at AI-coding speed and help harden gstack? -> Come work at YC — [ycombinator.com/software](https://ycombinator.com/software) -> Extremely competitive salary and equity. San Francisco, Dogpatch District. - ## GBrain — persistent knowledge for your coding agent [GBrain](https://github.com/garrytan/gbrain) is a persistent knowledge base for AI agents — think of it as the memory your agent actually keeps between sessions. GStack gives you a one-command path from zero to "it's running, my agent can call it." @@ -630,45 +211,40 @@ I open sourced how I build software. You can fork it and make it your own. /setup-gbrain ``` -Four paths, pick one: - -- **Supabase, existing URL** — your cloud agent already provisioned a brain; paste the Session Pooler URL, now this laptop uses the same data. -- **Supabase, auto-provision** — paste a Supabase Personal Access Token; the skill creates a new project, polls to healthy, fetches the pooler URL, hands it to `gbrain init`. ~90 seconds end-to-end. -- **PGLite local** — zero accounts, zero network, ~30 seconds. Isolated brain on this Mac only. Great for try-first; migrate to Supabase later with `/setup-gbrain --switch`. -- **Remote gbrain MCP** — your brain runs on another machine (Tailscale, ngrok, internal LAN) or a teammate's server; paste an MCP URL and bearer token. Optionally pair with a local PGLite for symbol-aware code search in split-engine mode. Best for cross-machine memory without standing up a local DB. - -After init, the skill offers to register gbrain as an MCP server for Claude Code (`claude mcp add gbrain -- gbrain serve`) so `gbrain search`, `gbrain put`, etc. show up as first-class typed tools — not bash shell-outs. - -**Keeping the brain current.** Run `/sync-gbrain` from any repo to re-index its code into gbrain (incremental by default, `--full` for a full reindex, `--dry-run` to preview). The skill registers the cwd as a federated source via `gbrain sources add`, runs `gbrain sync --strategy code`, and writes a `## GBrain Search Guidance` block to your project's CLAUDE.md so the agent prefers `gbrain search`/`code-def`/`code-refs` over Grep. The block is removed automatically if the capability check fails — no stale guidance pointing at tools that aren't installed. - -**Per-remote trust policy.** Each repo on your machine gets one of three tiers: - -- `read-write` — agent can search the brain AND write new pages back from this repo -- `read-only` — agent can search but never writes (best for multi-client consultants: search the shared brain, don't contaminate it with Client A's work while in Client B's repo) -- `deny` — no gbrain interaction at all - -The skill asks once per repo. The decision is sticky across worktrees and branches of the same remote. - -**GStack memory sync (different feature, same private-repo infra).** Optionally pushes your gstack state (learnings, CEO plans, design docs, retros, developer profile) to a private git repo so your memory follows you across machines, with a one-time privacy prompt (everything allowlisted / artifacts only / off) and a defense-in-depth secret scanner that blocks AWS keys, tokens, PEM blocks, and JWTs before they leave your machine. - -```bash -gstack-artifacts-init -``` - -**Running gstack in Conductor?** Conductor explicitly strips `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` from every workspace's process env, so paid evals and gbrain embeddings won't work out of the box. Set `GSTACK_ANTHROPIC_API_KEY` and `GSTACK_OPENAI_API_KEY` in Conductor's workspace env config instead — gstack's TS entry points promote them to canonical names at runtime. Full details and the contributor checklist for adding the import to new entry points: [Conductor + GSTACK_* env vars](USING_GBRAIN_WITH_GSTACK.md#conductor--gstack_-env-vars). - -**Full monty — every scenario, every flag, every bin helper, every troubleshooting step:** [USING_GBRAIN_WITH_GSTACK.md](USING_GBRAIN_WITH_GSTACK.md) +The four setup paths, the per-repo trust policy and memory sync, in brief: [docs/gbrain-quick-tour.md](docs/gbrain-quick-tour.md). **Full monty — every scenario, every flag, every bin helper, every troubleshooting step:** [USING_GBRAIN_WITH_GSTACK.md](USING_GBRAIN_WITH_GSTACK.md) Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guide) • [docs/gbrain-sync-errors.md](docs/gbrain-sync-errors.md) (error index) +## Platform notes + +**macOS:** everything runs. With the [Aside](https://aside.com) browser (macOS 15+) open and signed in, the browser skills, `/make-pdf` and `/diagram` use it and your real sessions; otherwise they use the bundled browser `./setup` builds. gstack never installs Aside. + +**Linux:** everything runs on the bundled browser; Aside is macOS-only, so the readiness probe says `NEEDS_ASIDE: Linux` once and the skills use gstack's own Chromium. If Chromium installs but cannot launch because the host blocks unprivileged user namespaces (Ubuntu 24.04+ AppArmor default), set `GSTACK_CHROMIUM_NO_SANDBOX=1`. + +**Windows users:** gstack works on Windows 11 via Git Bash or WSL. Aside is macOS-only, so on Windows (and Linux) the browser skills, `/make-pdf`, and `/diagram` always use gstack's bundled browser. Node.js is required in addition to Bun — Bun has a known bug with Playwright's pipe transport on Windows ([bun#4253](https://github.com/oven-sh/bun/issues/4253)). The browse server automatically falls back to Node.js. Make sure both `bun` and `node` are on your PATH. Native `/cso` additionally requires PowerShell (PowerShell 7 `pwsh` is preferred; Windows PowerShell 5.1 is the fallback) and Visual Studio 2022 Build Tools with the Desktop development with C++ workload; setup leaves that skill explicitly unavailable when they are absent. /cso is optional: if its native helper fails to build or publish, setup still finishes, says which step failed with the log path and retry command, and keeps an earlier helper when it has one (`GSTACK_STRICT_BUILD=1` makes that failure fatal, as CI does). + +**Known issue: Windows Smart App Control** ([#2595](https://github.com/garrytan/gstack/issues/2595)). gstack's compiled binaries (`browse`, `find-browse`, `design`, `pdf`, `gstack-global-discover`) are built on your machine and unsigned, so Windows 11 with Smart App Control on refuses to start them; Git Bash shows `Permission denied`. setup detects this, names the blocked binaries and the skills that need them, and `gstack-doctor` reports them as `blocked`. Today's workarounds are running gstack inside WSL, or turning Smart App Control off. Details: [troubleshooting](docs/troubleshooting.md#windows-smart-app-control). The sidebar terminal uses Consolas on Windows, so its text no longer renders spaced out. + +From PowerShell, `.\setup.ps1` (same arguments as `./setup`) checks that Git for Windows, Bun and Node.js are on PATH, prints the `winget` command for any that are missing, and otherwise runs `./setup` in Git Bash. + +On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file copies instead of symlinks because `ln -snf` produces frozen copies that don't refresh on `git pull`. **Re-run `cd ~/.claude/skills/gstack && ./setup` after every `git pull`** so your skill files match the repo. `setup` prints a one-line note reminding you. Unix and WSL keep symlinks and don't need the re-run. + ## Docs | Doc | What it covers | |-----|---------------| | [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) | +| [Why gstack](docs/why-gstack.md) | The story, the productivity numbers and their caveats, Karpathy's failure modes, parallel sprints | +| [Install details](docs/install.md) | Setup timing, requirements in detail, team mode, the hooks setup registers | +| [Other agents](docs/hosts.md) | OpenClaw and ClawHub, per-host notes, the Codex profile and model tiers, outside-review providers, the design binary's models | +| [QA without a webpage](docs/qa-without-a-webpage.md) | `/qa` and `/qa-only` on CLIs, APIs, jobs, workers and webhooks | +| [Standalone binaries](docs/standalone-binaries.md) | The standalone `gstack-*` CLIs (egress auditor, context bill, model benchmark, the review and evidence ledgers, the iOS QA daemon), and the CSO evaluation producer contract | +| [Privacy & Telemetry](docs/privacy.md) | What is sent, what never is, the update check, outside reviews, egress receipts | +| [Uninstall](docs/uninstall.md) | The uninstall script and manual removal | | [Diagrams & Document Formats](docs/howto-diagrams-and-formats.md) | Mermaid/excalidraw fences in PDFs, image sizing and safety defaults, `--to html\|docx`, `/diagram` triplets | | [First Production Deploy](docs/howto-bring-up-a-first-deploy.md) | What to do when `/setup-deploy` says nothing is deployed yet: hosting, env and secrets, DNS, checking it works, then back to `/setup-deploy` | | [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Ocean, Search Before Building, three layers of knowledge | +| [GBrain quick tour](docs/gbrain-quick-tour.md) | The four setup paths, MCP registration, the per-repo trust policy, memory sync, Conductor env vars | | [Using GBrain with GStack](USING_GBRAIN_WITH_GSTACK.md) | Every path, flag, bin helper, and troubleshooting step for `/setup-gbrain` | | [GBrain Sync](docs/gbrain-sync.md) | Cross-machine memory setup, privacy modes, troubleshooting | | [Architecture](ARCHITECTURE.md) | Design decisions and system internals | @@ -680,86 +256,7 @@ Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guid ## Privacy & Telemetry -gstack sends nothing about how you use it unless you opt in. Three kinds of -network traffic exist, and each has its own switch. - -**Telemetry (opt-in, default off).** On first run gstack asks once. You can say -no, and `gstack-config set telemetry off` turns it off at any time. - -- **What's sent, per skill run:** skill name, event type, outcome - (success/error/abort), duration, gstack version, OS and CPU architecture, - whether the browser was used, whether the run was live or a test, a session - id, the count of live session markers (below), the error class, the failed step, and an error message passed through gstack's - secret redaction (cut to 200 characters). `community` adds a stable device - ID; `anonymous` sends no ID. Prompt-injection detections add the attacking - page's domain, a payload hash, a confidence score, the detection layer and - the verdict. -- **What's never sent:** code, file paths, repo names, branch names, prompts, - or any other content you write. -- **What stays on your machine:** opted in, each run is also logged to - `~/.gstack/analytics/skill-usage.jsonl` (under your state root; see - [docs/state-root.md](docs/state-root.md)) with the repo name and branch, so - `gstack-analytics` can show your own dashboard. `gstack-telemetry-sync` - deletes those two fields (and, for `anonymous`, the device ID) before - anything is uploaded. With telemetry off, gstack logs no usage analytics at - all; nothing new is written there. -- **Where it goes:** [Supabase](https://supabase.com) (an open-source Firebase - alternative). The schema is in [`supabase/migrations/`](supabase/migrations/), - so you can verify what's collected. The publishable key in the repo is a - public key; row-level security denies direct access, and edge functions - enforce schema checks, event allowlists and field length limits. - -**Update check (on by default, regardless of telemetry).** Every interactive -skill start runs `gstack-update-check`. When its cache is stale (60 minutes after an -up-to-date result, 12 hours while an upgrade is pending, 10 minutes after a -failure) it asks GitHub for the latest version: a `git ls-remote` of -`github.com/garrytan/gstack` and a GET of the `VERSION` file from -`raw.githubusercontent.com`. GitHub sees your IP address; nothing about you or -your code is sent. With telemetry on, the same check also posts your gstack -version and OS to gstack's Supabase. Turn the check off with -`gstack-config set update_check false`. - -**Outside reviews (on by default; the first one prints a notice).** Plan -reviews, `/autoplan`, `/review` and `/ship` adversarial passes, `/ship`'s design -check, `/design-review`, `/design-consultation`, `/office-hours`, -`/document-release` and `/spec` can -ask a second model for an independent review: Codex under Claude Code, Claude -Code under Codex. That sends the review prompt and the relevant code, diff or -plan to that provider, using your own Codex or Claude Code login. The first -send on a machine says which provider and which account. Turn it off with -`gstack-config set codex_reviews disabled`; the binaries that send then refuse -(`not_run`, `consent_missing`) and start nothing, and an unreadable config -counts as disabled. `/codex` and `/claude-code` still run when you ask for -them by name. - -**Local state that is not telemetry.** gstack keeps a few local files whatever -your telemetry setting. They are functional state, not usage logs, and the -files are never sent: - -- **Session markers:** `~/.gstack/sessions/`, one empty file per live agent - session, touched at skill start and deleted after 120 minutes. The count lets - skills notice several sessions on one machine; with telemetry on, that number - is the session count above. -- **Project timeline:** `~/.gstack/projects//timeline.jsonl` records skill - starts and finishes with the branch name, for `/context-restore` and `/retro`. - It leaves the machine only if you turn on `artifacts_sync_mode full` - ([docs/gbrain-sync.md](docs/gbrain-sync.md)). - -**Every off-machine send is receipted.** Any gstack-initiated network send, -telemetry included, writes a hash-chained, tamper-evident receipt to -`~/.gstack/security/egress.jsonl` before the send; sensitive sinks refuse to -send at all if the receipt can't be written. Audit with `gstack-egress list`, -verify the chain with `gstack-egress verify` (exit 3 on tamper), and see the -standing consent settings with `gstack-egress grants`. The ledger records -attempted sends so accidents are auditable; it's an audit trail, not a network -firewall. - -**Optional third-party bridges are off by default and receipted too.** The one -that exists today, the [Memorable recall bridge](docs/memorable-workflow-memory.md), -hands your prompt to a locally installed vendor binary only after you run -`gstack-memorable enable`; every hand-off writes a receipt first, the consent -shows up in `gstack-egress grants`, and what the vendor then sends is -documented as the vendor's claim. +gstack sends nothing about how you use it unless you opt in. Telemetry is off by default and asked about once; the update check (two or three GitHub requests: a `git ls-remote`, a fetch of `VERSION` and, when that version equals yours but `main` has moved, a compare-API call; with telemetry on it also reports your gstack version and OS; on by default) turns off with `gstack-config set update_check false`; outside reviews (on by default; the first one prints a notice) send code to Codex or Claude Code under your own login and turn off with `gstack-config set codex_reviews disabled`. Every off-machine send writes a receipt you can audit with `gstack-egress list`. The full account, including the exact fields telemetry carries and the local state that is not telemetry: [docs/privacy.md](docs/privacy.md). ## Troubleshooting @@ -778,76 +275,6 @@ Paste its output into bug reports. [docs/troubleshooting.md](docs/troubleshooting.md): every gate message gstack prints, what it means, what was kept, and the command that fixes it. -**Skill not showing up?** Run `./setup --status` from your gstack checkout. It -prints every install (host, scope, version, skills directory, source checkout) -and, for a `stale`, `missing` or `unregistered` row, the exact command that fixes -it. The usual fix is to re-run setup from that row's source for that host, e.g. -`cd ~/.claude/skills/gstack && ./setup` (Claude) or `cd ~/gstack && ./setup --host codex`. -A project install lives in the project's `.claude/skills/gstack` or -`.agents/skills/gstack`; run its `setup` from inside the project. - -**Cursor freezes on load, or an agent's gstack skills went missing after an upgrade?** Each checkout now renders skills only for the agents installed from it (#1694). `./setup --status` lists them; `./setup --host ` adds one back. Pruned copies are in `~/.gstack/backups/host-renders/