diff --git a/.gitattributes b/.gitattributes index 713416057..e67042f0a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -37,3 +37,9 @@ bin/* text eol=lf *.gif binary *.ico binary *.pdf binary + +# The committed diagram-render bundle is hash-pinned (BUILD_INFO sha256); +# a CRLF rewrite on Windows checkout would break the drift test and change +# the content-addressed staged filename. +lib/diagram-render/dist/*.html text eol=lf +lib/diagram-render/dist/*.json text eol=lf diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml index c9aa6a293..667e12a24 100644 --- a/.github/workflows/evals.yml +++ b/.github/workflows/evals.yml @@ -162,6 +162,12 @@ jobs: permissions: contents: read pull-requests: write + # The comment upsert below calls the REST `/issues/{n}/comments` endpoints + # (gh api ... issues/comments). With GITHUB_TOKEN those are gated by the + # `issues` permission, not `pull-requests` — without it the GET returns 401 + # on every PR that produces eval artifacts (PRs with no artifacts exit + # early and never hit it, which is why this stayed hidden). See #1802 CI fix. + issues: write steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/make-pdf-gate.yml b/.github/workflows/make-pdf-gate.yml index 769fccd2b..cd07e26bc 100644 --- a/.github/workflows/make-pdf-gate.yml +++ b/.github/workflows/make-pdf-gate.yml @@ -4,6 +4,8 @@ on: branches: [main] paths: - 'make-pdf/**' + - 'lib/diagram-render/**' + - 'test/diagram-render-drift.test.ts' - 'browse/src/meta-commands.ts' - 'browse/src/write-commands.ts' - 'browse/src/commands.ts' @@ -81,7 +83,7 @@ jobs: which pdftotext && pdftotext -v 2>&1 | head -1 || true - name: Run make-pdf unit tests - run: bun test make-pdf/test/*.test.ts + run: bun test make-pdf/test/*.test.ts test/diagram-render-drift.test.ts - name: Run E2E gates (combined-features copy-paste + emoji render) env: diff --git a/.github/workflows/pr-title-sync.yml b/.github/workflows/pr-title-sync.yml index 6f5b3d3e5..4f94d4db9 100644 --- a/.github/workflows/pr-title-sync.yml +++ b/.github/workflows/pr-title-sync.yml @@ -1,7 +1,25 @@ name: PR Title Sync +# WHY pull_request_target (not pull_request): the default GITHUB_TOKEN is +# READ-ONLY on fork PRs under `pull_request`, so the title-sync backstop could +# never `gh pr edit` a fork/agent PR. `pull_request_target` runs in the base-repo +# context with a write token, which fixes fork coverage. +# +# WHY this is SAFE (pull_request_target is the most dangerous trigger): +# - We check out the BASE repo (no `ref:`), so the only code we execute is +# trusted base-repo infra (bin/gstack-pr-title-rewrite.sh). We NEVER check +# out or run PR-head/fork code. +# - Every attacker-controlled PR field (title, head repo, head sha) arrives via +# `env:` and is referenced as a shell-quoted "$VAR". We NEVER inline a +# `${{ github.event.pull_request.* }}` expression inside the run: script +# (that would execute a crafted title as shell). +# - The PR-head VERSION is read as DATA via the API (raw media type), from the +# head repo at the head sha — never by checking out the head. +# test/pr-title-sync-workflow-safety.test.ts is the static tripwire for all of +# the above and fails CI if any of it regresses. + on: - pull_request: + pull_request_target: types: [opened, synchronize, edited] paths: - 'VERSION' @@ -19,25 +37,62 @@ jobs: pull-requests: write if: github.actor != 'github-actions[bot]' steps: - - name: Checkout PR head + # Base repo only — trusted infra (the rewrite helper). No PR-head checkout. + - name: Checkout base repo (trusted) uses: actions/checkout@v4 with: fetch-depth: 1 - ref: ${{ github.event.pull_request.head.sha }} - name: Rewrite PR title to match VERSION env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUM: ${{ github.event.pull_request.number }} + # Attacker-controlled on fork PRs — env-only, never inlined into run:. OLD_TITLE: ${{ github.event.pull_request.title }} + BASE_REPO: ${{ github.repository }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail chmod +x ./bin/gstack-pr-title-rewrite.sh - VERSION=$(cat VERSION | tr -d '[:space:]') - NEW_TITLE=$(./bin/gstack-pr-title-rewrite.sh "$VERSION" "$OLD_TITLE") - if [ "$NEW_TITLE" = "$OLD_TITLE" ]; then - echo "Title already correct; no change." + + if [ "$HEAD_REPO" = "$BASE_REPO" ]; then IS_FORK=0; else IS_FORK=1; fi + + # Read the PR-head VERSION as data (raw bytes), from the head repo at + # the head sha. Guard the assignment itself: under `set -e` a bare + # `VERSION=$(...)` would abort the step before any later [ -z ] check. + if ! VERSION=$(gh api -H "Accept: application/vnd.github.raw" \ + "repos/$HEAD_REPO/contents/VERSION?ref=$HEAD_SHA" 2>/dev/null | tr -d '[:space:]'); then + VERSION="" + fi + + if [ -z "$VERSION" ]; then + # Same-repo read failure should never happen — fail loudly so we + # notice. A fork miss (public-contents quirk, private fork) is a + # convenience gap, not a gate — warn and skip so the check stays green. + if [ "$IS_FORK" = "0" ]; then + echo "::error::Could not read VERSION from same-repo PR head ($HEAD_SHA)." + exit 1 + fi + echo "::warning::Could not read VERSION from fork $HEAD_REPO ($HEAD_SHA); skipping title sync." + exit 0 + fi + + # The helper rejects a malformed VERSION (exit 2). Same policy: loud for + # same-repo, soft for forks. Never echo the raw (attacker-controlled) + # title — Actions still parses ::workflow-command:: from stdout. + if ! NEW_TITLE=$(./bin/gstack-pr-title-rewrite.sh "$VERSION" "$OLD_TITLE"); then + if [ "$IS_FORK" = "0" ]; then + echo "::error::Could not compute title for VERSION '$VERSION' on PR #$PR_NUM." + exit 1 + fi + echo "::warning::Could not compute title for fork PR #$PR_NUM; skipping." + exit 0 + fi + + if [ "$NEW_TITLE" = "$OLD_TITLE" ]; then + echo "PR #$PR_NUM title already correct; no change." exit 0 fi - echo "Rewriting: $OLD_TITLE -> $NEW_TITLE" gh pr edit "$PR_NUM" --title "$NEW_TITLE" + echo "PR #$PR_NUM title synced to VERSION." diff --git a/.gitignore b/.gitignore index 9fde8011f..5196c0d05 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,13 @@ dist/ browse/dist/ design/dist/ make-pdf/dist/ +# diagram-render ships its built bundle (offline-at-install premise, eng-review D2) +!lib/diagram-render/dist/ +!lib/diagram-render/dist/** bin/gstack-global-discover* .gstack/ .claude/skills/ +.claude/gstack-rendered/ .claude/scheduled_tasks.lock .claude/*.lock .agents/ @@ -37,3 +41,6 @@ supabase/.temp/ # Throughput analysis — local-only, regenerate via scripts/garry-output-comparison.ts docs/throughput-*.json + +# gbrain local source-staging dir (capability checks, source clones) — runtime artifact +.sources/ diff --git a/AGENTS.md b/AGENTS.md index a3d1fdb48..69651022d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,7 @@ End-to-end walkthrough: [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-t | `/guard` | Activate both careful + freeze at once. | | `/unfreeze` | Remove directory edit restrictions. | | `/make-pdf` | Turn any markdown file into a publication-quality PDF. | +| `/diagram` | English in, diagram out: mermaid source + editable .excalidraw + SVG/PNG, offline. | ## Build commands diff --git a/BROWSER.md b/BROWSER.md index 2c57f1d6e..affa0447d 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -212,8 +212,8 @@ from `snapshot`, or `@c` refs from `snapshot -C`. Full table: | Command | Description | |---------|-------------| -| `js ` | Run inline JavaScript expression in page context, return as string | -| `eval ` | Run JS from a file (path under /tmp or cwd; same sandbox as `js`) | +| `js [--out ] [--raw]` | Run inline JavaScript expression in page context, return as string. With `--out ` the result is written to disk instead of returned (a `data:*;base64,...` result is decoded to raw bytes unless `--raw`). `--out` makes the invocation a WRITE (needs `write` scope, never allowed over the tunnel). | +| `eval [--out ] [--raw]` | Run JS from a file (path under /tmp or cwd; same sandbox as `js`). `--out`/`--raw` behave as for `js`. | | `css ` | Computed CSS value | | `attrs ` | Element attributes as JSON | | `is ` | State check: visible, hidden, enabled, disabled, checked, editable, focused | @@ -527,10 +527,16 @@ window is being controlled. ### What "GStack Browser" means Not your daily Chrome — a Playwright-managed Chromium with custom branding -in the Dock and menu bar, anti-bot stealth (sites like Google and NYTimes -work without captchas), a custom user agent, and the gstack extension -pre-loaded via `launchPersistentContext`. Your regular Chrome with your tabs -and bookmarks stays untouched. +in the Dock and menu bar (the `.app` name, Dock icon, and tray, NOT the UA +string), always-on Layer C anti-bot stealth (most JS-observable automation +tells are masked, so many anti-bot-protected sites load cleanly), a +stock-Chrome user agent that reports the underlying Chromium version, and the +gstack extension pre-loaded via `launchPersistentContext`. The UA no longer +carries a `GStackBrowser` suffix — that branding string was itself a +high-entropy tell, so the browser now reports a plain `Chrome/` UA. +Deepest-layer CDP-protocol detection still gets through (Google can still +trigger captchas; see the CDP-patch item in `TODOS.md`). Your regular Chrome +with your tabs and bookmarks stays untouched. ### When to use headed mode @@ -581,13 +587,42 @@ A running daemon with config A meeting a new invocation with config B exits 1 with a `browse disconnect` hint instead of silently restarting and dropping tab state, cookies, or sessions. -**Stealth scope.** When `--headed` or `--proxy` are set, `$B` masks -`navigator.webdriver` only — via Chromium's -`--disable-blink-features=AutomationControlled` plus a small init script. -We do NOT fake `navigator.plugins`, `navigator.languages`, or `window.chrome` -— modern fingerprinters check those for consistency, and synthesizing fixed -values can flag MORE bot-like, not less. ChromeDriver's `cdc_` runtime -artifacts and the Permissions API patch are still cleaned up. +**Stealth scope (Layer C, always on).** Every context — headless `launch`, +`--headed`/`--proxy`, `handoff`, and the `useragent`/`viewport --scale` +rebuild (`recreateContext`) — gets the full Layer C mask, no opt-in flag. +Layer C masks `navigator.webdriver`, restores the `window.chrome.*` shape +(`runtime`, `app`, `csi`, `loadTimes`), aligns `Notification.permission` +with the Permissions API, reports a per-install +`hardwareConcurrency`/`deviceMemory` from the host profile, sweeps the known +Selenium/Phantom/Nightmare/Playwright globals, and installs a +`Function.prototype.toString` proxy so every patched getter reports +`[native code]` even under the depth-3 recursion check. It still does NOT +fake `navigator.plugins` or `navigator.languages` — modern fingerprinters +cross-check those for consistency, and synthesizing fixed values flags MORE +bot-like, not less. ChromeDriver's `cdc_`/`__webdriver` runtime artifacts and +the Permissions notifications tell are also cleaned up on every path. + +`GSTACK_STEALTH=extended` (also accepts `1` or `true`; off by default) layers +six more aggressive patches on top — WebGL renderer spoof, a faked +`navigator.plugins` PluginArray, `navigator.mediaDevices`. That mode actively +lies and can break sites that reflect on those properties; use it only when +the default triggers detection. For gbrowser builds with the C++ patches, the +`GSTACK_*` host-profile env (GPU vendor/renderer, UA-CH platform/model, +hardware) emits the Pack 1 `--gstack-gpu-vendor` / `--gstack-gpu-renderer` / +`--gstack-ua-platform` / `--gstack-ua-model` / `--gstack-hw-concurrency` / +`--gstack-device-memory` switches that push the GPU/UA-CH/hardware spoof down +to native code, and `GSTACK_CDP_STEALTH=on` (or `1`/`true`) emits the Pack 2 +`--gstack-suppress-prepare-stack-trace` switch (closes the Cloudflare +`Error.prepareStackTrace` canary). On stock Playwright Chromium every one of +these switches is a safe no-op. + +`launchHeaded` / `handoff` also strip Playwright's automation-tell launch +defaults via `ignoreDefaultArgs` (`STEALTH_IGNORE_DEFAULT_ARGS`): +`--enable-automation` (the "Chrome is being controlled by automated test +software" infobar), `--disable-extensions`, +`--disable-component-extensions-with-background-pages`, +`--disable-popup-blocking`, `--disable-component-update`, and +`--disable-default-apps`. **Container support.** `--headed` on Linux without `DISPLAY` walks the display range (`:99`, `:100`, ...) until `xdpyinfo` reports a free slot, @@ -1164,6 +1199,11 @@ the global `~/.gstack/browser-skills/foo/` only inside project-a. | `GSTACK_BROWSE_MAX_HTML_BYTES` | 52428800 (50MB) | `load-html` size cap | | `GSTACK_SECURITY_OFF` | unset | Emergency kill switch — disable ML classifier | | `GSTACK_SECURITY_ENSEMBLE` | unset | Set to `deberta` for 3-classifier ensemble (721MB download) | +| `GSTACK_STEALTH` | unset | Set to `extended` (also accepts `1`/`true`) to layer six aggressive patches (WebGL spoof, faked plugins, mediaDevices) on top of Layer C. Actively lies; can break sites. | +| `GSTACK_CDP_STEALTH` | unset | Set to `on`/`1`/`true` to emit `--gstack-suppress-prepare-stack-trace` (gbrowser Pack 2 / B11 C++ patch only; no-op on stock Chromium) | +| `GSTACK_GPU_VENDOR`, `GSTACK_GPU_RENDERER`, `GSTACK_GPU_CHIPSET` | unset | Per-install GPU spoof fed to the Pack 1 WebGL/UA-CH C++ patches. Set by gbd from the host profile; emitted as `--gstack-gpu-vendor` / `--gstack-gpu-renderer` / `--gstack-ua-model` cmdline switches only when present. | +| `GSTACK_PLATFORM` | unset | Host platform classification (`MacARM`/`MacIntel` → `macOS`, `Win32` → `Windows`, `Linux*` → `Linux`) emitted as `--gstack-ua-platform` | +| `GSTACK_HW_CONCURRENCY`, `GSTACK_DEVICE_MEMORY` | host profile (fallback 8) | Per-install `hardwareConcurrency`/`deviceMemory` reported by Layer C and emitted as `--gstack-hw-concurrency` / `--gstack-device-memory` for the worker-navigator C++ patch | --- @@ -1179,7 +1219,7 @@ browse/ │ ├── proxy-config.ts # --proxy URL parsing + cred resolution (URL vs env, fail-fast on both) │ ├── 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 # navigator.webdriver mask + cdc_ cleanup + Permissions API patch +│ ├── 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 │ ├── browse-client.ts # Canonical SDK — what skills import as _lib/browse-client.ts │ ├── snapshot.ts # AX tree → @e/@c refs → Locator map; -D/-a/-C handling │ ├── read-commands.ts # Non-mutating: text, html, links, js, css, is, dialog, ... diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2d5e3d4..893bac6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,971 @@ # Changelog +## [1.58.3.0] - 2026-06-18 + +## **GBrowser masks the full set of automation tells by default, on every path a page can reach.** +## **Layer C stealth is always on, carries a per-install hardware identity, and survives the toString depth-3 trick.** + +GBrowser's headless and headed Chromium now ship "Layer C" anti-detection by default, with no opt-in flag. Where the old default masked only `navigator.webdriver`, the browser now also restores the full `window.chrome.*` shape (runtime, app, csi, loadTimes), aligns `Notification.permission` with the Permissions API, reports a per-install `hardwareConcurrency`/`deviceMemory` from the host profile, sweeps the known Selenium/Phantom/Nightmare/Playwright globals, and installs a `Function.prototype.toString` proxy so every patched getter reports `[native code]` even under the depth-3 recursion check. The aggressive `GSTACK_STEALTH=extended` mode (WebGL spoof, faked plugins, mediaDevices) still exists, now layered on top of Layer C rather than replacing it. And stealth applies on all four context-creation paths, so a `useragent` change, a `viewport --scale`, or a headless-to-headed handoff hands a site a fully masked page every time. + +### The numbers that matter + +Source: `bun test browse/test/stealth-layer-c.test.ts browse/test/stealth-webdriver.test.ts browse/test/stealth-extended.test.ts browse/test/browser-manager-unit.test.ts` (80 tests, real Chromium for the runtime checks). + +| Capability | Before (v1.58.1.0) | After (v1.58.3.0) | +|---|---|---| +| Automation tells masked by default | 1 (navigator.webdriver) | 7 categories (webdriver, window.chrome.*, Notification, per-install hardware, toString-native, automation-global sweep, cdc/Permissions) | +| Context paths that apply stealth | 2 (launch, launchHeaded) | 4 (+ handoff, + recreateContext) | +| toString integrity | not addressed | survives the depth-3 `[native code]` check | +| Hardware identity | generic Chromium default | per-install, from the host profile | +| Stealth tests | none dedicated | 80 passing (incl. real-Chromium runtime) | + +By default the browser now masks seven categories of automation tell instead of one, on every path a page can reach, not just the first launch. + +### What this means for builders + +If you drive GBrowser to dogfood, scrape, or QA against anti-bot-protected targets, your sessions look like a real per-install Chrome out of the box. There is no `GSTACK_STEALTH` flag to remember, and no silent gap where a routine `useragent` or `viewport --scale` strips the mask. For gbrowser builds with the Pack 1 C++ patches, set the `GSTACK_*` host-profile env (gbd does this) to push the GPU/UA-CH/hardware spoof down to native code; on stock Playwright Chromium the same call is a safe no-op. + +### Itemized changes + +#### Added +- Always-on Layer C stealth (`buildStealthScript`): webdriver mask, `window.chrome.{runtime,app,csi,loadTimes}` shape, `Notification.permission` alignment, per-install `hardwareConcurrency`/`deviceMemory`, a `Function.prototype.toString` proxy that holds up under the depth-3 `[native code]` check, and a static sweep of Selenium/Phantom/Nightmare/Playwright globals. +- `buildGStackLaunchArgs`: per-install `--gstack-*` cmdline switches (GPU vendor/renderer, UA-CH platform/model, hardware concurrency/memory) for gbrowser's Pack 1 C++ patches, emitted only when the matching `GSTACK_*` env is set so stock Chromium is unaffected. +- Real-Chromium runtime coverage: webdriver, chrome.* shape, Notification/Permissions pairing, toString depth-3, per-install hardware, and the extended-mode blend (80 stealth tests). + +#### Changed +- Stealth applies on every context-creation path (`launch`, `launchHeaded`, `handoff`, `recreateContext`), so a `useragent`, `viewport --scale`, or handoff keeps the full mask. +- The cdc_/`__webdriver` cleanup and the Permissions notifications shim live in `applyStealth`, so headless and handoff get the same `Notification.permission`/`permissions.query` consistency as the headed path. +- `GSTACK_STEALTH=extended` layers on top of Layer C; the always-on default does not fake `navigator.plugins` (the opt-in mode still does, as the documented "may break sites" escape hatch). +- `--gstack-suppress-prepare-stack-trace` is opt-in via `GSTACK_CDP_STEALTH=on`, so the switch never reaches a Chromium that does not understand it. +- `--disable-blink-features=AutomationControlled` comes from one shared `STEALTH_LAUNCH_ARGS` constant across every launch path. + +## [1.58.1.0] - 2026-06-14 + +## **Local evals stop lying. Spawned `claude` test children run in a sealed clean room,** +## **and in Conductor every decision is a plain-text brief you answer with a letter.** + +Two things shipped here. First, the local E2E harness is now hermetic by default: +every spawned agent (claude -p, the real-PTY plan-mode runner, the Agent SDK +runner, plus the codex and gemini runners) gets an allowlist-scrubbed environment, +a fresh seeded `CLAUDE_CONFIG_DIR`, a temp `GSTACK_HOME`, and `--strict-mcp-config`. +Before this, a dev machine leaked the operator's `~/.claude` config, MCP servers +(gbrain, Conductor), skills, `~/.gstack` decision logs, and `CONDUCTOR_*`/`CLAUDECODE` +env into every child, so local eval results disagreed with CI for reasons that had +nothing to do with the code under test. Now local signal matches CI. Set +`EVALS_HERMETIC=0` to debug against real operator state. + +Second, in a Conductor session gstack no longer fights Conductor's flaky +AskUserQuestion tool. It detects the session and renders every decision as a prose +brief, a labeled question with a recommendation, per-option completeness scores, and +"reply with a letter," enforced by a PreToolUse hook that denies the tool and +redirects to prose. Destructive confirmations demand an explicit typed answer. + +Agents that launch long eval runs get `gstack-detach`: a SIGTERM-proof, idle-sleep-proof +wrapper (fresh session + `caffeinate`) with a machine-wide lock so concurrent +worktrees serialize instead of saturating the model API, run-scoped logs, and a +guaranteed `EXIT=` sentinel so a poller never mistakes silence for success. + +### The numbers that matter + +Measured against the gate eval suite on a contaminated dev box (gbrain MCP up, live +Conductor session, sibling worktrees). Reproduce: `bun test` (free unit + wiring +tripwire) and `EVALS=1 EVALS_TIER=gate bun test test/skill-e2e-hermetic-canary.test.ts`. + +| Metric | Before | After | Δ | +|--------|--------|-------|---| +| Spawned-child env | full operator `process.env` | allowlist-scrubbed | sealed | +| Runners hermeticized | 0 of 5 | 5 of 5 | +5 | +| Operator MCP servers visible to child | all (gbrain, Conductor) | 0 (`--strict-mcp-config`) | isolated | +| Config isolation proof | none | poisoned-operator sentinel canary | falsifiable | +| Long eval runs surviving a turn-boundary SIGTERM | no | yes (`gstack-detach`) | survives | + +The clean room is falsifiable, not asserted: a `hermetic-sentinel` gate canary +plants a poisoned operator config (a user `CLAUDE.md` + an MCP server) and fails if +the child can see any of it, and a free static tripwire fails CI if any runner +reverts to a raw `process.env` spread. + +### What this means for contributors + +Run evals locally and trust the result. You no longer have to push to CI to find +out whether a failure was real or just your machine bleeding context into the agent. +Three latent bugs the old harness hid surfaced the moment the suite ran clean and +are fixed: a coverage-judge that scored carved skills against half a document, an +ios-qa daemon test that collided on a shared pidfile under concurrency, and an +operational-learning fixture missing a lib it imports. Start a run with +`bun run eval:bg:gate`; flip `EVALS_HERMETIC=0` only when you deliberately want your +real `~/.claude` in the loop. + +### Itemized changes + +#### Added +- **Hermetic E2E environment** (`test/helpers/hermetic-env.ts`): allowlist env + builder (process basics, network/proxy vars, named `ANTHROPIC_*` auth, per-runner + `extraAllow`), pure `promotedEnv()` shared with `lib/conductor-env-shim.ts`, a + sync-memoized singleton temp dir (`/.claude` keeps the plan-file path + contract), a seeded `.claude.json` for non-interactive first run, and pid-aware GC + of crashed runs. Default-on; `EVALS_HERMETIC=0` restores the legacy env AND drops + `--strict-mcp-config`. +- **Two gate-tier isolation canaries** (`test/skill-e2e-hermetic-canary.test.ts`): + `hermetic-canary` asserts env redirect + scrub + zero MCP servers + nonzero + API-key cost from the Bash tool_result (not model prose); `hermetic-sentinel` + proves the child cannot see a planted poisoned operator config. +- **Static wiring tripwire** (`test/hermetic-wiring.test.ts`): free-tier invariants + that fail CI if any of the five runners drops `hermeticChildEnv()`, the gated + `--strict-mcp-config`, or leaks `process.env` through a callsite override. +- **`gstack-detach`** + `eval:bg` / `eval:bg:all` / `eval:bg:gate` / `eval:bg:periodic` + scripts: detached, SIGTERM-proof, `caffeinate`-wrapped eval runs with a machine-wide + lock, per-run logs under `~/.gstack-dev/eval-runs/`, a watchdog, and an `EXIT=` + sentinel. +- **Conductor prose AskUserQuestion**: when a Conductor session is detected, every + decision renders as a prose brief (labeled question, recommendation, per-option + completeness, reply-with-a-letter), enforced by a PreToolUse hook that denies the + tool and redirects. Auto-decide preferences still apply first; destructive + confirmations require an explicit typed answer. Installed for Conductor even in + non-interactive setup, with an upgrade migration for existing installs. + +#### Changed +- All five E2E runners (`session-runner`, `claude-pty-runner`, `agent-sdk-runner`, + `codex-session-runner`, `gemini-session-runner`) spawn children through + `hermeticChildEnv()`. The Agent SDK runner now receives a COMPLETE hermetic env + via `Options.env` (the old "never pass env: to the SDK" rule was partial-env + replacement; a complete env is safe). +- `hermetic-env.ts` is a global touchfile, so any change to it selects every E2E + + judge test. +- CLAUDE.md documents hermetic-by-default local evals and retires the stale SDK env + warning. + +#### Fixed +- The workflow LLM-judge now re-appends body-carved `sections/*.md` after the marker + slice, so carved skills (document-release) are judged on the full workflow the + agent executes instead of a half-document. +- ios-qa daemon scenarios use unique pidfiles, fixing `already_running` collisions + under `bun test --concurrent`. + +## [1.58.0.0] - 2026-06-12 + +## **Your documents grow diagrams. Mermaid and excalidraw fences render as real pictures,** +## **and make-pdf now ships single-file HTML and Word output from the same markdown.** + +Put a ` ```mermaid ` fence in your markdown and `make-pdf` renders it as a crisp +vector diagram, fully offline, with the source preserved for round-trips. A broken +fence prints a loud red diagnostic block with the parse error, never silent raw +code. The new `/diagram` skill goes the other way: describe a flow in English and +get a triplet back, the mermaid source, an editable `.excalidraw` file you can open +at excalidraw.com in the hand-drawn style, and rendered SVG + PNG. Images got the +same care: local paths inline automatically and never truncate, phone photos +downscale to print resolution instead of blowing up the file, and a wide small-text +diagram promotes itself onto a vertically centered landscape page inside an +otherwise portrait document. One markdown file now exports three ways: +`--to pdf | html | docx`, where html is one self-contained file with zero network +references. Type is bigger across the board (12pt body, 56pt cover titles), TOC +links actually jump, and `--strict` turns missing, remote, out-of-tree, or +oversized images into hard CI failures. + +### The numbers that matter + +Measured on this repo's README (5,940 words, lists, code, screenshots, one +diagram fence) and the free gate suite. Reproduce: `make-pdf generate README.md +--cover --toc` and `bun test make-pdf/test/`. + +| Metric | Before | After | Δ | +|--------|--------|-------|---| +| A mermaid fence in your PDF | raw code block | vector diagram | rendered | +| Output formats from one markdown | 1 (pdf) | 3 (pdf, html, docx) | +2 | +| Network requests at render time | up to 1 per remote image | 0 by default | sealed | +| Wide-diagram handling | shrunk into portrait | own centered landscape page | rotated | +| Free make-pdf gate tests | 121 | 189 | +68 | +| README → 29-page PDF with diagram | n/a | 4.4s | one command | + +The sealed-network number is the one to notice: the mermaid and excalidraw +runtimes are vendored into a 9.2MB sha-pinned bundle, so rendering works on a +plane and a tracking pixel in pasted markdown fetches nothing. + +### What this means for your documents + +The diagram you describe in English stays editable forever: `/diagram` writes the +source, you embed the source in markdown, and every export renders it fresh. Stop +pasting screenshots of diagrams into documents. Run `/diagram` for the picture, +` ```mermaid ` for the document, and `--to html` when the reader doesn't want a PDF. + +### Itemized changes + +#### Added +- ` ```mermaid ` and ` ```excalidraw ` fences render as inline vector SVG in pdf + and html output (docx embeds them as 300dpi PNGs). Fence options: `title="..."` (caption + aria-label), + `render=false` (keep as code), `page=landscape|portrait` (orientation override). + Render failures produce a visible diagnostic block with the parse error. +- `/diagram` skill: English in, editable triplet out (`.mmd` source, + `.excalidraw` scene, SVG + PNG). Flowcharts convert to fully editable + excalidraw scenes; other mermaid types render with an explicit limitation note. +- `lib/diagram-render/`: vendored offline bundle (mermaid 11.12.2, excalidraw + 0.18.0, exact pins), deterministic build, committed dist with sha256 + source + fingerprint, drift tests, THIRD-PARTY-LICENSES. +- `--to pdf|html|docx` output formats. HTML is one self-contained file (inline + SVG diagrams, data-URI images, zero network refs, screen-readable). DOCX is a + content-fidelity export with diagrams embedded as 300dpi PNGs and alt text. +- Per-image directives: `![x](a.png){width=full|50%|3in}` and + `{page=landscape|portrait}`. +- Conservative auto-landscape: wide, small-text, diagram-like images get their + own vertically centered landscape page (aspect ≥ 1.8, width over ~2.5x the + content box, diagram-ish alt word). Directives override in both directions. +- `--strict` for CI: missing images, remote images, out-of-tree image reads, + oversized files, and non-regular files fail the run instead of degrading to + placeholders. +- `docs/howto-diagrams-and-formats.md`: the full walkthrough, fences to formats. + +#### Changed +- Typography scale: 12pt body, 26pt h1, 56pt poster cover with 13pt meta, 12pt + TOC entries, larger code and tables. Auto-hyphenation is off so copy-paste + yields clean words. +- Local images inline as data URIs with byte-probed dimensions and never + truncate; oversized photos downscale to print resolution at inline time; + repeated images are read once. +- TOC links resolve in every format (headings get real anchor ids); the screen + layer hides print-only page-number dots in HTML output. +- Remote images are blocked with a visible placeholder unless `--allow-network` + is passed; out-of-tree image reads (including via symlink) warn loudly. +- `make-pdf preview` prints a note when the document contains fences or local + images that only `generate` renders fully. + +#### Fixed +- Relative image paths render correctly in PDFs (previously resolved against the + wrong base and could show as broken boxes). +- Fenced code inside lists survives the render byte-for-byte; indented fences + keep their list placement. +- Documents containing `$&`-style sequences in diagram labels render exactly; + Windows drive-letter image paths resolve as local files; malformed + percent-encoded image URLs degrade gracefully instead of failing the run. +- Per-side margins (`--margin-left` etc.) are honored on documents containing + landscape pages. + +#### For contributors +- 68 new free-tier gates (fence extraction, image policy, landscape promotion + with negative fixtures, format contracts, bundle drift) plus a paid gate-tier + /diagram triplet test and a periodic authoring-quality judge. +- make-pdf-gate CI now covers `lib/diagram-render/**` and the drift test; the + committed bundle is pinned to LF in .gitattributes. +- Fixed the `operational-learning` E2E fixture (bin scripts now ship with the + lib module they import). + +## [1.57.10.0] - 2026-06-10 + +## **Codex review now runs by default everywhere it matters.** +## **One switch governs it, and it falls back to Claude when Codex is missing or unauthed.** + +Codex cross-model review used to be inconsistent. `/review` and `/ship` ran it +automatically, but plan reviews hid it behind a "Want an outside voice?" question +you had to say yes to every time, `/document-release` never ran it at all, and every +entry point only checked whether the `codex` binary existed, not whether it was +logged in. Now `codex_reviews` is one master switch (default `enabled`) that governs +Codex review across `/review`, `/ship`, `/plan-ceo-review`, `/plan-eng-review`, +`/plan-design-review`, `/plan-devex-review`, `/document-release`, and `/autoplan`. +The plan-review outside voice runs automatically. `/document-release` gets a new +Codex pass that checks your docs against what actually shipped. Every call site now +detects install AND auth separately, and degrades to a Claude subagent with a clear +one-line reason instead of silently skipping. Turn the whole thing off with one +command: `gstack-config set codex_reviews disabled`. + +### The numbers that matter + +Verified by the gate-tier E2E evals that exercise these exact paths +(`codex-offered-ceo-review`, `codex-offered-eng-review`, `document-release`, +`codex-review-findings`), all green this run. + +| Metric | Before | After | Δ | +|--------|--------|-------|---| +| Skills where Codex review runs by default | 2 | 8 | +6 | +| Prompts to get a plan-review outside voice | 1 (opt-in each time) | 0 (automatic) | -1 | +| Codex readiness detection | install only | install + auth | sharper | +| Master switches to disable it all | 0 (per-skill only) | 1 (`codex_reviews`) | +1 | +| `/document-release` Codex doc audit | none | doc-vs-diff pass | new | + +When Codex is installed but not logged in, you used to get nothing on the paths that +checked only `command -v codex`. Now you get a named reason ("Codex installed but not +authenticated, using Claude subagent") and the review still happens. A typo on the +switch (`gstack-config set codex_reviews disabledd`) is rejected and your existing +setting is preserved, so a fat-finger can never silently turn paid Codex calls on or +off. + +### What this means for you + +If you run gstack day to day, you stop deciding whether to get a second model's eyes +on every plan and every release. It is just there, on by default, the way the strong +reviewers already worked on diffs. If you do not have Codex set up, nothing breaks: +you get the Claude outside voice instead, with a one-line note telling you how to add +Codex for true cross-model coverage. If you want it gone, one command turns off all +eight surfaces at once. + +### Itemized changes + +#### Added +- **`codex_reviews` as the master switch** for Codex review across `/review`, `/ship`, + `/document-release`, all four plan reviews, and `/autoplan` (`bin/gstack-config`). + Default `enabled`. Invalid values on `set` are rejected with the existing value + preserved, so a typo cannot flip paid Codex calls. +- **`/document-release` Codex doc audit** (`generateCodexDocReview`): reviews the + docs you touched against the release diff for stale claims, undocumented new + surface, and over/under-sold CHANGELOG entries. Informational, with an explicit + apply-fixes decision point. Never auto-edits docs. +- **`codexPreflight()` shared helper** (`scripts/resolvers/constants.ts`): one + self-contained bash block that reads the switch, sources the probe, checks install + and auth, and emits a single canonical mode (`ready` / `not_installed` / + `not_authed` / `disabled`). + +#### Changed +- **Plan-review outside voice is default-on**, not opt-in. The "Want an outside + voice?" question is gone; it runs automatically and falls back to a Claude subagent + when Codex is unavailable. Incorporating its findings still requires your explicit + approval (cross-model tension is presented, never auto-applied). +- **Adversarial review detects auth, not just install** (`generateAdversarialStep`): + distinct "not installed" vs "not authenticated" guidance. The 200-line threshold + for the heavier structured `codex review` is unchanged. +- **`/autoplan` honors `codex_reviews=disabled`** in its Phase 0.5 preflight, so the + switch is truly global. + +#### Fixed +- Three `gstack-config` tests asserted `get`/`list` print empty for unset keys; the + tool falls back to the documented defaults table. Assertions now match real behavior. + +#### For contributors +- Size-budget guards widened for the default-on outside-voice prose, each with a + rationale comment (`test/helpers/carve-guards.ts`, `test/helpers/parity-harness.ts`). +- Static guards added: plan reviews must not carry the opt-in question and must render + the default-on voice; `/document-release` must carry the doc review; the codex host + strips all of it (`test/skill-validation.test.ts`). + +## [1.57.9.0] - 2026-06-09 + +## **Your gstack checkout stays clean when gbrain is installed.** +## **Brain-aware skill blocks render to an untracked spot, never into tracked source.** + +Before this, finishing a Conductor or dev-workspace setup with gbrain installed +rewrote 16 planning and review SKILL.md files in place, adding 326 lines of +brain-aware blocks straight into tracked source. Your working tree came back dirty, +one stray `git add` away from committing a token regression for everyone who does +not run gbrain. Now `gen-skill-docs --out-dir` renders the brain-aware variant into +an untracked per-workspace directory, and `bin/dev-setup` repoints the workspace's +skill symlinks at it. The dev workspace gets the full gbrain experience (context-load +and save-to-brain blocks live at runtime), while the tracked SKILL.md files stay +byte-for-byte canonical. To turn the blocks on across all your projects' Claude +sessions, `gstack-config gbrain-refresh` now renders them into your global install, +guarded so it never mutates a symlinked or non-gstack directory. + +### The numbers that matter + +Structural facts of the change, verifiable from the diff plus `bun run gen:skill-docs` +(zero drift) and the new behavioral test (`test/gen-skill-docs-out-dir.test.ts`). + +| When gbrain is installed | Before | After | +|---|---|---| +| Tracked SKILL.md files dirtied by dev-setup | 16 (+326 lines) | 0 | +| Where brain-aware blocks render in a dev workspace | in-place, tracked source | `.claude/gstack-rendered/`, untracked | +| Brain-aware blocks across other projects | re-run `./setup` or hand-edit | `gstack-config gbrain-refresh` (idempotent) | +| "Is gbrain usable" check | per-caller JSON grep, can read stale state | `gstack-gbrain-detect --is-ok` (one live gate) | + +The section-path rewrite is surgical: only `~/.claude/skills/gstack//sections/` +references move to the render dir, so `bin/` and `docs/` references still resolve to +the install. + +### What this means for you + +If you develop gstack with gbrain on, `git status` is clean again after setup, and +you can stop fishing brain-block drift out of your commits. After a +`git reset --hard` deploy of your install, re-run `gstack-config gbrain-refresh` to +restore the machine-wide blocks (it is idempotent, and the deploy note in CLAUDE.md +spells this out). + +### Itemized changes + +#### Added +- `gen-skill-docs --out-dir `: render the Claude SKILL.md + sections into a + separate directory instead of in place, rewriting only the section-base path so + section reads resolve to the render. Default (no flag) output is unchanged. +- `gstack-gbrain-detect --is-ok`: live-detection exit-code gate (0 iff gbrain is + usable), so setup, dev-setup, and gstack-config share one check. +- `gstack-config gbrain-refresh` now renders brain-aware blocks into the global + install (`~/.claude/skills/gstack`), guarded against symlinked or non-gstack + targets and self-documenting about the `reset --hard` re-run cycle. + +#### Changed +- `bin/dev-setup` renders the brain-aware variant into `.claude/gstack-rendered` + (gitignored) and repoints workspace skill symlinks at it; the worktree stays + canonical. `GSTACK_SKIP_GBRAIN_REGEN` is passed inline to the nested setup, never + exported. +- `setup` honors `GSTACK_SKIP_GBRAIN_REGEN` (skips the in-place brain regen on dev + trees) and writes detection state to a PID-unique tmp so concurrent workspaces + cannot clobber it. +- `scripts/dev-skill.ts` refreshes the workspace render on template change, only + when the render dir already exists. +- `bin/dev-teardown` removes the untracked render. + +#### For contributors +- New tests: `test/gen-skill-docs-out-dir.test.ts` (behavioral: worktree unchanged, + blocks rendered, section paths rewritten), `test/dev-setup-render-isolation.test.ts` + and `test/gbrain-refresh-install-render.test.ts` (static tripwires), plus + `--is-ok` coverage in `test/gbrain-detect-shape.test.ts`. + +## [1.57.8.0] - 2026-06-09 + +## **`browse` is now the one Chromium on the box, for offline rendering too.** +## **`js`/`eval --out ` writes a render straight to disk, so skills stop bundling their own puppeteer.** + +You can now turn your own local HTML or JSON into a PNG (or any bytes) on disk +through the same headless `browse` Chromium you already run, with no second +browser install. `js "" --out out.png` and `eval script.js --out out.png` +write the evaluate result to a file instead of returning it. When the result is a +base64 data URL (the shape Excalidraw exports, og-image generators, and card +renderers hand back), `--out` decodes it to raw bytes for you; pass `--raw` to +write the literal string. Malformed base64 errors loudly instead of writing a +corrupt file, and missing parent directories are created. This closes the gap that +made local-render skills each `npm i puppeteer` and download a drifting second +Chromium. + +### The numbers that matter + +No synthetic benchmark — these are structural facts of the change, verifiable from +the diff and a one-line smoke (`browse load-html` → `screenshot --selector` / +`js --out`). + +| For a skill that rasterizes local HTML/JSON | Before | After | +|---|---|---| +| Chromium installs per box | 2+ (browse + each skill's own puppeteer) | 1 (shared `browse`) | +| Getting a PNG from a render function | `evaluate` → multi-MB data URL over the CLI channel → hand-decode base64 → write | `js --out` decodes and writes server-side; only a short status crosses the channel | +| Render-to-file primitive | none | `js`/`eval --out [--raw]` | + +The blessed offline path is documented in the browse skill: visual output goes +through `screenshot --selector` (the picture never crosses the CDP wire), and bytes +a function returns go through `js --out`. + +### What this means for you + +If you write skills that draw diagrams, cards, or og-images, point them at `browse` +and delete the bundled Chromium. One version to pin, one daemon to manage. `--out` +is treated as a write everywhere it matters: it needs the `write` scope, is blocked +over the pair-agent tunnel, and is gated in watch mode, so a remote agent can never +use it to write to your disk. + +### Itemized changes + +#### Added +- **`js` / `eval --out ` render-to-file** (`browse/src/read-commands.ts`). + Writes the evaluate result to disk and returns a short `... result written: + ( bytes)` status. A `data:;base64,...` result is decoded to raw bytes + (case-insensitive header parse, split on the first comma, base64-charset validated + before decode); `--raw` forces a literal write. Parent directories are created. +- **`--raw` flag** to bypass data-URL decoding and write the literal result string. +- **Offline render mode docs** in the browse skill: an explicit headless, no-proxy, + no-Xvfb path with a worked example showing visual (`screenshot --selector`) vs + bytes (`js --out`), a puppeteer→browse cheatsheet row, and a "don't bundle your + own Chromium" note (also in CONTRIBUTING.md). + +#### Changed +- **`--out` is a per-invocation WRITE capability** (`browse/src/server.ts`). + `js`/`eval` stay read commands, but an `--out` invocation requires the `write` + scope, is never dispatchable over the tunnel surface (`canDispatchOverTunnel` now + consults args), and counts as a mutation for watch-mode and tab-ownership gates. + +#### For contributors +- New tests: `parseOutArgs`/`hasOutArg` unit coverage (`--out`/`--out=`, `--raw`, + repeats, missing value, ordering), `--out` render-to-file integration (large + string, data-URL→PNG, `--raw`, malformed-base64, outside-safe-dir, mkdir, eval + parity, byte-for-byte null/undefined), and tunnel-gate guards proving `--out` + is never tunnel-dispatchable. + +## [1.57.7.0] - 2026-06-08 + +## **Every plan review now ends by telling you, in one line, whether anything is still unresolved.** +## **The GSTACK REVIEW REPORT closes with the open decisions, or "NO UNRESOLVED DECISIONS" in plain sight, before you approve.** + +When a plan-review skill (/plan-ceo-review, /plan-eng-review, /plan-design-review, +/plan-devex-review, and /codex) finishes and hands you the plan to approve, its report +now ends with a mandatory unresolved-decisions verdict. If decisions are still open, it +lists each one and what breaks if you ship it deferred. If nothing is open, it prints the +exact line NO UNRESOLVED DECISIONS. A token-reduction pass had made this line optional, so +a clean plan and a plan hiding an open question rendered the same. Now the line is never +omitted, it is always the last thing you read before the approval prompt, and the approval +gate refuses to let the plan through without it. + +### What changed, before and after + +| At plan-approval time | Before | After | +|---|---|---| +| Clean plan | usually no unresolved line | `NO UNRESOLVED DECISIONS` as the final line | +| Plan with open decisions | unresolved line optional, often dropped | `**UNRESOLVED DECISIONS:**` + one bullet per open item | +| Approval gate (ExitPlanMode) | checked the line "if applicable" | blocks unless the unresolved status is the final line | +| /plan-devex-review review log | never written, gate uncheckable | written, so the dashboard and report see its data | + +The unresolved count across reviews is computed without double-counting the review that +just ran, using the same 7-day freshness window as the Review Readiness Dashboard. + +### What this means for you + +Every approve-plan moment now carries an explicit verdict on open questions, so a missed +ambiguity cannot slip through looking like a clean plan. If you run the plan-review skills +or /autoplan, you will see the unresolved status as the closing line of every report. +Nothing to configure. Upgrade and your next plan review shows it. + +### Itemized changes + +#### Added +- **Mandatory unresolved-decisions status in the GSTACK REVIEW REPORT.** Generated into + all six report consumers (/plan-ceo-review, /plan-eng-review, /plan-design-review, + /plan-devex-review, /codex, /devex-review) from `scripts/resolvers/review.ts`. The report + always ends with either the exact unbolded sentinel `NO UNRESOLVED DECISIONS` or a + `**UNRESOLVED DECISIONS:**` bullet block listing each open item; never omitted, always + the final line. +- **Blocking approval gate.** The EXIT PLAN MODE GATE now refuses ExitPlanMode unless the + report's final non-whitespace line is the unresolved status (no "if applicable" escape). +- Static and E2E tests pinning the mandatory status across every report consumer and + gate-bearing skill, so a future compression pass cannot silently drop it again. + +#### Fixed +- **/plan-devex-review never logged a review entry.** It carried the approval gate but + never called `gstack-review-log`, so the gate's "review log was called" check was + structurally unsatisfiable and its data was invisible to the Review Readiness Dashboard + and the report. It now logs with the correct timestamp and DX fields. + +#### For contributors +- Rebased the parity-suite size baseline v1.53.0.0 to v1.57.7.0 (captures current union + sizes; keeps the per-skill 1.05 ratio so future bloat is still caught). Regenerated the + three ship golden fixtures left stale by #1909. The frozen v1.44.1 integrity anchor and + the v1.47 size-budget baseline are untouched. + +## [1.57.6.0] - 2026-06-07 + +## **Eight community-filed bugs fixed in one wave, four of them security guards that were quietly failing open.** +## **Your redaction gate now catches modern OpenAI keys, and `/ship`'s adversarial review stops choking on your own security tests.** + +This is a fix wave. The throughline: guards that reported success while doing nothing. +The secret-redaction gate that every `/spec`, `/ship`, `/cso`, and `/document-*` run +passes through was blind to modern `sk-proj-`/`sk-svcacct-`/`sk-admin-` OpenAI keys and +silently dropped its size cap on a bad flag. The cross-project learnings trust gate was +an allowlist on paper and a denylist in code, so untrusted rows leaked between projects. +The destructive-action classifier waved through "rotate the database password." Each one +looked like it was protecting you. None of them were. All four now fail closed, with +tests that pin the exact case that used to slip by. Three more fixes clear silent +crashes and skipped reviewers, and `/ship`'s adversarial pass no longer trips Anthropic's +usage policy when it reads your repo's own attack-payload fixtures. + +### The numbers that matter + +Reproduce with `bun test test/redact-engine.test.ts test/gstack-learnings-search.test.ts test/one-way-doors.test.ts test/diff-scope.test.ts test/brain-cache-roundtrip.test.ts`. + +| Guard / path | Before | After | +|---|---|---| +| `sk-proj-`/`sk-svcacct-`/`sk-admin-` OpenAI keys | zero findings (HIGH fails open) | blocked, with prose false-positive guards | +| `gstack-redact --max-bytes ` | NaN silently disables the size cap | rejected at the CLI; engine backstop holds | +| Cross-project learnings with no `trusted` field | imported (denylist bug) | excluded (true allowlist) | +| "rotate the database password" | classified two-way (auto-approvable) | classified one-way (always asks) | +| `.mjs/.cjs/.mts/.cts`-only PRs | backend reviewer skipped | backend reviewer runs | +| `_meta.json` missing `last_refresh` | brain-cache crashes (TypeError) | degrades to a cold cache | +| Safety-skill hooks on Claude Code 2.1.162 | every Edit/Write errored | hooks resolve and run | +| `/ship` adversarial review over security fixtures | denied by usage policy | runs, fixtures read in summary mode | + +The redaction one is the sharpest: a project/service-account/admin OpenAI key pasted +into a spec or PR body used to sail straight through the gate. Now it blocks, and the +calibration is pinned so hyphenated prose like "the sk-learning-rate schedule" does not +false-positive and wedge your ship. + +### What this means for you + +If you rely on the redaction guard or the cross-project learnings gate, they now do what +the docs always said. If you run `/ship` on a repo that tests its own security guards, +adversarial review stops dying on contact with your fixtures. And if you are on Claude +Code 2.1.162, `/guard`, `/freeze`, and `/careful` work again instead of erroring on every +edit. Upgrade and re-run anything that touched these paths. + +### Itemized changes + +#### Fixed +- **Redaction misses modern OpenAI keys (#1868).** `openai.key` (HIGH/block) used a + contiguous-alphanumeric pattern that stopped at the first `-`/`_`, so base64url-bodied + `sk-proj-`/`sk-svcacct-`/`sk-admin-` keys produced no finding and failed open through + every redaction sink. Replaced with explicit bare-vs-prefixed alternation; added + positive and false-positive tests. Reported by @jbetala7. +- **Redaction size cap fails open on a bad flag (#1824).** A malformed `--max-bytes` + parsed to `NaN`, and `byteLen > NaN` is always false, silently disabling the + fail-closed oversize guard; a negative value blocked everything. The CLI now rejects + non-integer / non-positive values, and the engine falls back to the default cap as a + backstop. Reported by @jbetala7. +- **Cross-project learnings trust gate leaked (#1745).** `gstack-learnings-search + --cross-project` is documented as an allowlist but was coded as `trusted === false`, + admitting any row missing the `trusted` field. Flipped to `trusted !== true`. Reported + by @jbetala7. +- **Destructive-action classifier missed "rotate ... password" (#1839).** The `rotate` + keyword pattern omitted `password` while its `revoke`/`reset` siblings included it, so + the most common credential-rotation phrasing classified as a reversible two-way + question. Added `password` to the alternation. +- **Review Army skipped backend reviewer on ESM/CJS PRs (#1810).** `gstack-diff-scope` + matched only `*.ts|*.js`; a PR touching only `.mjs/.cjs/.mts/.cts` reported no backend + scope. Added the four module extensions. Reported by @jbetala7. +- **Brain-cache crash on a partial `_meta.json` (#1879).** `loadMeta` returned parsed + JSON verbatim; a file missing `last_refresh` crashed three consumers with a TypeError. + Added an object-shape guard and map normalization; missing schema/endpoint identity now + forces a safe rebuild rather than trusting a stale file. Reported by @jbetala7. +- **Safety-skill hooks broken on Claude Code 2.1.162 (#1871).** `guard`, `freeze`, and + `careful` frontmatter hooks used `${CLAUDE_SKILL_DIR}`, which CC 2.1.162 no longer + populates, so every Edit/Write/Bash errored. Anchored the hook commands to the + installed checkout path. Reported by @omariani-howdy. +- **`/ship` adversarial review denied on own security fixtures (#1899).** The Claude + adversarial subagent reasoned "like an attacker" over the full diff; when the diff + included the repo's own attack-payload regression fixtures, Anthropic's real-time + usage-policy safeguards denied the call. The subagent now carries authorized-defensive + -testing framing and reads fixture/test files in summary mode (no raw payload bytes), + stating so explicitly. Reported by @bmajewski. + +#### For contributors +- `#1882` (skills hardcode `~/.claude/skills/gstack/`, breaking non-`gstack` install + dirs) is filed as the top item in `TODOS.md`. It was scoped out of this wave once it + proved to be a host-config/preamble change touching all 52 skills, distinct from the + `#1871` hook fix it was originally paired with. + +## [1.57.5.0] - 2026-06-07 + +## **Your agent now keeps its decisions, not just its code.** +## **The durable calls you make, and the "why" behind them, are captured, curated, and resurfaced across sessions, with no daemon to run.** + +Every session you and the agent settle real decisions: pick an architecture, cut a scope, choose a tool, reverse an earlier call. Until now that reasoning lived only in a transcript that scrolls away, so the next session re-litigates settled questions or loses the "why." This release adds an institutional decision memory. Durable decisions land in an append-only, event-sourced store, the scope-relevant ones surface automatically at session start, and you can search them any time. It is file-only and works with gbrain off; when gbrain is up you can add semantic recall on top. The planning and ship skills capture their own key calls so the high-value decisions get recorded without anyone remembering to. Separately, `/sync-gbrain` learned to build the cross-reference call graph and to heal a crashed daemon's stale lock instead of wedging every sync. + +### The numbers that matter + +No speed benchmark here, the win is capability and reliability. These are the real shape of the release (`git diff 1.57.0.0..HEAD`, `bun test`): + +| Metric | Value | +|--------|-------| +| New commands | 2 (`gstack-decision-log`, `gstack-decision-search`) | +| Session-start read cost | O(active) bounded snapshot, not a full-history scan | +| Works with gbrain OFF | Yes, every capture/curate/resurface path is files + bins only | +| New source | ~2,550 lines across 26 files | +| New tests | 117 across the decision store + gbrain stages | + +Resurfaced decision text is treated as data, not instructions (datamarked at the render boundary), secrets are blocked on write, and `redact` expunges a decision from every read path. The whole loop degrades cleanly: turn gbrain off and you still capture, curate, and resurface. + +### What this means for you + +Start a session tomorrow and the agent already knows what you settled and why, instead of asking again or quietly reversing it. Log a call with `gstack-decision-log`, reverse one with `--supersede`, pull the relevant history with `gstack-decision-search`. CEO, eng, spec, and ship reviews record their decisions for you. Run `/sync-gbrain` and a crashed autopilot no longer blocks your next sync. + +### Itemized changes + +#### Added +- **Cross-session decision memory.** An event-sourced (`decide`/`supersede`/`redact`) store at `~/.gstack/projects//decisions.jsonl`. "Active" is computed, never a mutable flag, so the history stays honest and tolerant of dangling references. +- **`gstack-decision-log`** — capture a durable decision, reverse one (`--supersede `), expunge an accidental secret (`--redact `), or rewrite the log to its active set (`--compact`). Non-interactive, injection-sanitized, blocks HIGH and MEDIUM secrets on write. +- **`gstack-decision-search`** — read active decisions, scope-filtered to the current branch/issue, with `--recent N`, `--scope`, `--query`, `--all`, `--json`. Add `--semantic` (with `--query`) to append related hits from gbrain memory when it is up; it degrades silently to the reliable file results when gbrain is off. +- **Session-start resurfacing.** Context Recovery shows the scope-relevant active decisions at the top of a session, from a bounded snapshot so it stays fast as the log grows. +- **Skill capture.** `/plan-ceo-review`, `/plan-eng-review`, `/spec`, and `/ship` record their structured decisions (accepted scope, architecture verdict, filed spec, version bump) automatically. +- **A `## Cross-session decision memory` section in CLAUDE.md** documenting when and how to capture and resurface. +- **`/sync-gbrain` call-graph build (`--dream`).** Builds the symbol cross-reference graph behind a lock-free gate, with an honest outcome guard that reports a degraded no-op as WARN rather than a false success. + +#### Changed +- Decision text that resurfaces into agent context is datamarked (code fences, `---` banners, `<|role|>`/`` tags, chat turn-prefixes, and Unicode line terminators are neutralized) so stored text can never masquerade as instructions. +- `/sync-gbrain` pin guidance is accurate for current gbrain, and the worktree-scoped `.gbrain-source` pin routes code queries correctly. + +#### Fixed +- `/sync-gbrain` no longer wedges forever on a crashed autopilot daemon's stale lock: it reads the holder pid, confirms liveness, and ignores a dead one (it stays conservative when it cannot tell). + +#### For contributors +- New shared `lib/jsonl-store.ts` (injection-reject + atomic single-line append + tolerant read) backs both the learnings and decision stores, so the sanitization path is audited in one place. +- `lib/bin-context.ts` shares slug/branch/flag plumbing across the decision bins. + +## [1.57.4.0] - 2026-06-08 + +## **The completeness principle is now Boil the Ocean, matching the post it came from.** +## **One name across the ETHOS file, every skill, and the developer-profile dial.** + +The principle that tells gstack to do the complete thing was called "Boil the Lake" in +`ETHOS.md` and in every generated skill, with the ocean cast as the anti-pattern. The +developer-profile system and the completeness intro link already used "boil the ocean" +as the good, ship-the-whole-thing pole. So the same idea carried two opposite framings +depending on where you read it. This renames the principle to Boil the Ocean everywhere +and reframes the metaphor: the ocean is the complete destination, and lakes are the +boilable units you ship on the way there. The guidance is identical. Only the name and +the framing prose changed. + +### The numbers that matter + +Reproduce with `git diff v1.57.3.0..HEAD --stat`. + +| Property | Before | After | +|---|---|---| +| Principle name in ETHOS + every skill | "Boil the Lake" | "Boil the Ocean" | +| Name vs. the `scope_appetite` dial ("boil the ocean" = complete) | split | unified | +| Files updated | — | 63 (ETHOS, CLAUDE, README, resolvers, templates, generated SKILL.md) | +| Runtime behavior change | — | none, text only | + +The one number that matters is zero: no behavior changed. A reviewer reading `ETHOS.md` +no longer hits "ocean" as the thing to avoid in one section and the thing to aim for in +the next. + +### What this means for you + +You get the same complete-the-work recommendations, now under the name from Garry's +"Boil the Oceans" post. The metaphor reads straight through: the ocean is the goal, +lakes are how you get there one boil at a time, and only genuinely unrelated +multi-quarter migrations sit outside scope. Nothing to do on your end. + +### Itemized changes + +#### Changed +- `ETHOS.md` section 1 is renamed to "Boil the Ocean" and reframed so the ocean is the + complete destination and lakes are the boilable first units, not the ceiling. +- The "Completeness Principle" header injected into every tier-2+ skill now reads + "Boil the Ocean," with prose to match. +- `CLAUDE.md` and `README.md` references updated to the new name. + +#### For contributors +- Source of the rename lives in the preamble resolvers + (`generate-completeness-section.ts`, the `composition.ts` skip-list, and + `generate-lake-intro.ts`); all SKILL.md files are regenerated from them. +- Unit assertions (`skill-validation`, `terse-build`) and the three ship golden + fixtures updated to the new header. + +## [1.57.3.0] - 2026-06-07 + +## **Every PR `/ship` opens gets the version stamped into its title, fork and agent PRs included.** +## **The rule rides in the always-loaded part of the skill now, and a guard keeps it there.** + +`/ship` stamps `vX.Y.Z.W` onto the title of every PR or MR it creates or updates, so +the version is the first thing you read in the PR list. That rule now lives in the +always-loaded core of the ship skill instead of an on-demand section, so the agent +applies it whether or not it opened the section that spells out the full procedure. +A CI workflow backs this up: it rewrites a title to match VERSION on every PR that +bumps the version, and it now reaches fork and agent PRs too, which a read-only token +could never touch before. Two free tests lock the behavior in so it cannot drift on +the next refactor. + +### The numbers that matter + +Reproduce with `bun test test/carve-section-ordering.test.ts test/pr-title-sync-workflow-safety.test.ts` +and `bun run eval:select`. + +| Property | Before | After | +|---|---|---| +| Where the title rule loads | on-demand section only (since v1.54.0.0) | always-loaded skeleton + on-demand detail | +| Fork / agent PR title sync | none (read-only token under `pull_request`) | covered via hardened `pull_request_target` | +| Test proving the rule stays put | none | carve-guard registry asserts it on every PR | +| CI injection guard for the title workflow | none | static tripwire fails CI on unsafe patterns | + +The title workflow now runs with a write token in the base-repo context but never +checks out or executes PR-head code, and every attacker-controlled field reaches the +script through `env:`, never inlined. A static test fails CI if either rule regresses. + +### What this means for you + +Ship a branch and the PR shows up titled `v1.57.3.0 fix: ...` without you touching it, +even when the PR came from a fork. The agent no longer needs to read the right section +at the right moment for the version to land in the title, and the next person who slims +the ship skill cannot quietly strand the rule again, because a free test on every PR +checks that it is still there. + +### Itemized changes + +#### Added +- Carve-guard coverage for the ship PR-title invariant: the registry now asserts the + `v$NEW_VERSION` rule and the title helper stay in the always-loaded skeleton, while + the full create and update procedure stays in the on-demand section. +- Static CI-safety test for the title-sync workflow that fails the build if it checks + out PR-head code or inlines an attacker-controlled PR field into a shell step. + +#### Changed +- The PR/MR title-version rule is always-loaded in `/ship` again, so the version + prefix lands on every PR the workflow creates or updates. +- The PR title-sync CI workflow now covers fork and agent PRs through a hardened + `pull_request_target` trigger (base-repo checkout only, PR fields passed via `env:`, + VERSION read as data from the PR head). + +#### Fixed +- A path token in the ship PR-body section that rendered literally instead of resolving + now uses the correct helper path, so the Linked Spec auto-detect step runs as written. + +## [1.57.2.0] - 2026-06-08 + +## **When the question picker breaks mid-skill, gstack asks in plain text instead of stalling.** +## **Every skill detects a dead AskUserQuestion and falls back to a full decision brief you answer by typing a letter.** + +AskUserQuestion is how every gstack skill asks you to decide. When the host's question +tool fails at runtime, which Conductor's MCP integration currently does intermittently, +skills used to stall or hard-block. Now each skill detects the failure, works out +whether a human is actually present, and if so re-renders the exact same decision as a +text message: a plain-English explanation of the issue, a completeness score on each +choice, and a recommendation with its reason, one paragraph per choice. You answer by +typing a single letter. Headless eval runs still block cleanly (no human to answer); +orchestrator sessions keep auto-choosing. This whole release was built and reviewed +through that fallback, because the Conductor tool was down the entire session. + +### The numbers that matter + +No production benchmark for a reliability path like this. These are the behavior and +coverage facts, verifiable with `bun test test/gstack-session-kind.test.ts +test/resolver-ask-user-format.test.ts test/auq-error-fallback-hook.test.ts`. + +| When AskUserQuestion fails | Before | After | +|---|---|---| +| Interactive session (human present) | stall / hard BLOCK | full prose decision brief, answer by letter | +| Headless eval / CI | BLOCK | BLOCK (unchanged, correct) | +| Orchestrator (OpenClaw) session | undefined | auto-choose recommended (contract kept) | +| Session kinds detected | 0 | 3 (interactive / headless / spawned) | +| New tests guarding the path | 0 | 34 | + +The text brief is not a degraded stub. It carries the same three things the picker +shows: a clear explanation of what is being decided, a `Completeness: X/10` on every +choice, and a recommendation with the reason it wins. + +### What this means for you + +If your host's question tool flakes out, a skill no longer dies on you. You get the +same decision to make, in text, and you reply with a letter. Nothing changes when the +tool works normally. If you run gstack headless, those sessions still block on a needed +question exactly as before, so eval determinism is intact. + +### Itemized changes + +#### Added +- `gstack-session-kind` classifies each session as interactive, headless, or spawned, + echoed as `SESSION_KIND` at skill start so any skill can branch on it. +- Plain-text fallback for AskUserQuestion: on a tool failure in an interactive session, + the skill renders the full decision brief (issue ELI10 + per-choice completeness + + recommendation) as markdown you answer by typing a letter, then stops and waits. +- A defensive hook that, when an AskUserQuestion call errors, reminds the agent to run + the fallback for the current session kind. + +#### Changed +- AskUserQuestion is still sent as a normal tool call; the prose path applies only when + the tool is unavailable or erroring, and never on a `[plan-tune auto-decide]` result. + +#### Fixed +- Section-loading tests use the canonical kebab test names, so the test-coverage gate + matches them. +- External-host doc-freshness checks are deterministic, no longer dependent on a prior + full regeneration. + +#### For contributors +- The eval/E2E runners set `GSTACK_HEADLESS=1` so headless runs classify correctly; + interactive-path suites opt out per-run. +- Per-skill `maxSizeRatio` override in the carve-guards registry; `document-release` + gets 1.08 headroom for the cross-cutting preamble addition while every other skill + keeps the 1.05 ceiling. + +## [1.57.0.0] - 2026-06-07 + +## **Three more heavyweight skills load lighter, and every carved skill finally has a test that proves it loads.** +## **`/cso`, `/document-release`, and `/design-consultation` shed ~49KB of always-loaded prose; CI now blocks any carve that ships without its guards.** + +gstack splits its biggest skills into a small always-loaded skeleton plus on-demand +sections that load only when a step needs them. This release carves three more, +`/document-release`, `/design-consultation`, and `/cso`, so the first time you invoke +them the agent reads far less. It also closes a gap from the earlier carves: only two +of six already-carved skills had a test proving an agent actually reads the section it +was told to read. Now all nine carved skills are guarded the same way, and CI blocks +any future carve that ships without its guards. `/cso` got extra care: its mode +dispatch and false-positive-filtering rules stay always-loaded, so a security audit +can never run with a rule stranded in an unread section. + +### The numbers that matter + +Measured with `wc -c /SKILL.md`; the skeleton+sections union is reproduced by +`bun test test/parity-suite.test.ts test/skill-size-budget.test.ts`. + +| Skill | Always-loaded before | After | Δ | +|---|---|---|---| +| /design-consultation | 80,719 B | 59,229 B | **−27%** | +| /document-release | 59,256 B | 45,797 B | **−23%** | +| /cso | 79,383 B | 65,117 B | **−18%** | +| Carved skills with a section-load guard | 2 of 6 | 9 of 9 | **full coverage** | + +Total always-loaded prose across the three skills drops about 49KB (~12K tokens) on +first invoke, with nothing lost: every line moved into an on-demand section the +skeleton points at, and the parity suite checks the union still contains it. + +### What this means for you + +Run `/cso`, `/document-release`, or `/design-consultation` and the agent does less +reading before it starts working, so the session stays leaner. The carve pattern is +now safe to extend: a free static test runs on every PR and a behavioral test runs +weekly to prove the agent reads each section, so future slimming can't quietly drop +behavior. Nothing about how you invoke these skills changed. + +### Itemized changes + +#### Added +- Canonical carved-skill guard registry (`test/helpers/carve-guards.ts`): one source of truth for which skills are carved and what each must preserve. `parity-harness.ts` and `skill-size-budget.ts` derive their carved-skill lists from it. +- Carve guard suite: data-driven static ordering test, behavioral section-loading test (periodic), a completeness meta-guard that fails CI if a carved skill lacks its guards, and negative tests proving the guards actually fire. +- `/cso`, `/document-release`, and `/design-consultation` carved into skeleton + on-demand sections. + +#### Changed +- `/cso` keeps its mode dispatch (`## Arguments`, `## Mode Resolution`), always-run phases, and false-positive-filtering exceptions always-loaded; an earliest-use invariant enforces that dispatch appears before any on-demand read. + +#### For contributors +- Redaction, taxonomy, and parity content tests now read the skeleton+sections union so relocated prose still counts toward coverage. +- Real-session section-read canary deferred to TODOS (the deterministic guards ship first). + +## [1.56.1.0] - 2026-06-03 + +## **`/sync-gbrain` can no longer delete your repo. Cleanup now refuses any directory it cannot prove it created.** + +A `/sync-gbrain` memory sync could recursively delete your entire working tree. A +crashed import left a checkpoint pointing at the repo root, the next sync +"resumed" into it, and the cleanup step `rm -rf`'d it, taking uncommitted and +untracked work with it. This release closes that path and fixes three more bugs +hiding in the same resume machinery: cleanup now deletes only directories it can +prove are gstack-minted staging dirs, the remote-http transcript dir is never +touched, an interrupted import actually keeps its checkpoint so the next run +resumes instead of restaging, and a resumed run no longer marks files that failed +to import as successfully ingested. + +### The numbers that matter + +Source: `bun test test/regression-1611-gbrain-sync-resume.test.ts` on this branch. + +| Metric | Before | After | Δ | +|--------|--------|-------|---| +| Repo-root `rm -rf` reachable | yes | no | closed | +| Proof required before delete | none | 5 checks | realpath + direct-child + name + .git tripwire + minted-marker | +| Resume after a timed-out import | broken (dir deleted) | works | fixed | +| Failed files mislabeled "ingested" on resume | yes | no | fixed | +| Resume regression-test assertions | 9 | 64 | +55 | + +The guard is fail-closed: anything it cannot prove it owns is left on disk (a few +seconds of re-staging next run) rather than deleted. That asymmetry is the design +- a missing marker can cost a little work, never your data. + +### What this means for you + +If you use `/sync-gbrain`, a crashed or timed-out import can no longer cost you +uncommitted work. Resume now does what it always claimed: a large sync that times +out picks up where it left off next run instead of starting over, and files that +failed to import get retried instead of silently skipped. Nothing to configure. +Upgrade and keep syncing. + +### Itemized changes + +#### Fixed +- **`/sync-gbrain` could `rm -rf` your repo root.** A poisoned resume checkpoint + (dir = the repo, written when an import was interrupted while the repo was the + working directory) was adopted as the staging dir and recursively deleted. A + single fail-closed ownership check now guards every staging delete and every + resume: a path must resolve cleanly, be a direct child of `~/.gstack` named + `.staging-ingest-*`, contain no `.git`, and carry a marker file gstack minted. + Anything else is refused. Contributed by @diazMelgarejo (cyre). +- **Remote-http syncs no longer churn (or scare you).** The persistent transcript + dir that the brain sync pushes is no longer routed through staging cleanup, so + it stops being deleted on every run and stops emitting a false "preventing data + loss" warning. +- **A timed-out import now actually resumes.** Previously the run said "checkpoint + preserved" but then deleted the staging dir, so the next run always restaged. + The staging dir is now kept when a checkpoint points at it, and the message is + honest when there is nothing to resume. +- **Resume no longer hides import failures.** A resumed run could mark files that + failed to import as ingested, so they were never retried. Failures now map back + to their source files on resume and get another pass. + +#### For contributors +- New `lib/staging-guard.ts` exports `checkOwnedStagingDir()`, the single + fail-closed predicate shared by the deletion chokepoint and the resume gate. It + returns the realpath-resolved canonical path so callers delete exactly what they + validated (closes a symlink TOCTOU). `makeStagingDir()` tears down and rethrows + if its marker write fails, so a marker-less dir can never leak. The + `#1611` resume regression suite grew to 64 assertions covering the poison + matrix, the remote-http gate, timeout-preserve, and resume failure-mapping. + ## [1.56.0.0] - 2026-06-03 ## **Five heavy skills now load their bulk on demand, the shared question preamble slimmed corpus-wide, and a paranoid test suite proves the questions never got worse.** diff --git a/CLAUDE.md b/CLAUDE.md index dc62ad561..984844902 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,11 +31,26 @@ use Codex's own auth from `~/.codex/` config — no `OPENAI_API_KEY` env var nee `lib/conductor-env-shim.ts`) promotes `GSTACK_ANTHROPIC_API_KEY` / `GSTACK_OPENAI_API_KEY` to their canonical names inside gstack's TS binaries. Tests run through gstack entrypoints inherit this promotion automatically. -Don't echo the key value to stdout, logs, or shell history. When passing to a -test's Agent SDK, do NOT pass `env: {...}` to `runAgentSdkTest` — the SDK's -auth pipeline doesn't pick up the key the same way when env is supplied as an -object (confirmed failure mode). Mutate `process.env.ANTHROPIC_API_KEY` -ambiently before the call and restore in `finally`. +Don't echo the key value to stdout, logs, or shell history. The historical +"never pass `env:` to `runAgentSdkTest`" rule is retired: the failure was +partial-env replacement (the SDK's `Options.env` REPLACES the child's entire +environment, so an object without the key broke auth). The runner now always +passes a COMPLETE hermetic env with per-test `env:` merged last, so per-test +overrides are safe; ambient `process.env.ANTHROPIC_API_KEY` mutation also +still works (the env builder reads process.env at call time). + +**Hermetic local E2E (default).** Every E2E runner (claude -p, PTY, Agent +SDK, codex, gemini) spawns children through `test/helpers/hermetic-env.ts`: +allowlist-scrubbed env (operator `CONDUCTOR_*`, `CLAUDE_*`, `GSTACK_*`, +`MCP_*`, `GBRAIN_*`, and credentials like `GH_TOKEN` never reach children), +a fresh seeded `CLAUDE_CONFIG_DIR` (no operator `~/.claude` CLAUDE.md / +MCP servers / skills), a temp `GSTACK_HOME`, and `--strict-mcp-config`. +Local eval signal matches CI. Debug against real operator state with +`EVALS_HERMETIC=0` (restores the legacy env AND drops the strict-MCP flag). +Per-test `env:` overrides merge last, so deliberate contamination +(`CONDUCTOR_WORKSPACE_PATH`, per-test `GSTACK_HOME`) keeps working. Wiring +is pinned by `test/hermetic-wiring.test.ts` (static tripwire) and two +gate-tier canaries in `test/skill-e2e-hermetic-canary.test.ts`. E2E tests stream progress in real-time (tool-by-tool via `--output-format stream-json --verbose`). Results are persisted to `~/.gstack-dev/evals/` with auto-comparison @@ -137,7 +152,7 @@ gstack/ ├── setup # One-time setup: build binary + symlink skills ├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly) ├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs -├── ETHOS.md # Builder philosophy (Boil the Lake, Search Before Building) +├── ETHOS.md # Builder philosophy (Boil the Ocean, Search Before Building) └── package.json # Build scripts for browse ``` @@ -776,8 +791,10 @@ When estimating or discussing effort, always show both human-team and CC+gstack | Research / exploration | 1 day | 3 hours | ~3x | Completeness is cheap. Don't recommend shortcuts when the complete implementation -is a "lake" (achievable) not an "ocean" (multi-quarter migration). See the -Completeness Principle in the skill preamble for the full philosophy. +is achievable. Boil the ocean — the complete thing is the goal; only genuinely +unrelated multi-quarter migrations are separate scope, never an excuse for a +shortcut. See the Completeness Principle in the skill preamble for the full +philosophy. ## Search before building @@ -826,6 +843,34 @@ them. Report progress at each check (which tests passed, which are running, any failures so far). The user wants to see the run complete, not a promise that you'll check later. +## Running evals as an agent: always detach (SIGTERM-proof) + +When **you (an agent/harness)** launch a long eval/benchmark run, run it through +`bin/gstack-detach` — NEVER as a plain backgrounded Bash task. A plain background +task lives in the harness's process group, so a SIGTERM ("polite quit") on a turn +boundary, a stopped Monitor, or an interruption kills the run mid-flight (observed: +`script "test:gate" was terminated by signal SIGTERM` ~40 min into a run). On macOS +the run can also die to idle-sleep. `gstack-detach` fixes both: a fresh session +(escapes the group SIGTERM) wrapped in `caffeinate -i` (blocks idle-sleep). + +- Use the `eval:bg*` scripts (`eval:bg`, `eval:bg:all`, `eval:bg:gate`, + `eval:bg:periodic`) — they wrap the eval command in `gstack-detach` with the + machine-wide `gstack-evals` lock (concurrent worktrees serialize instead of + saturating the shared model API), a per-tier watchdog, and a **run-scoped** log + under `~/.gstack-dev/eval-runs/` (no shared-`/tmp` collision). Each prints its + log path. Or call `gstack-detach [--lock NAME] [--timeout SECS] [--label LBL] -- + ` directly for any long agent job. Export `ANTHROPIC_API_KEY` first (never + pass keys in argv). +- Then **poll the printed logfile** with a death-aware watcher: break on the + guaranteed `### gstack-detach EXIT= ###` sentinel (success AND failure are + both marked, so silence is never mistaken for success). The detached run survives + even if your watcher gets reaped, so re-checking the log always works. +- Why the lock: a shared dev box with several Conductor worktrees will rate-limit + the model API if two eval suites run at once (15-way concurrency each), which + mass-times-out E2E tests. The lock makes the second run WAIT, not collide. +- Humans running `bun run test:evals` foreground in their own terminal don't need + this — Ctrl-C is intended there. Detachment is for agent-launched runs only. + ## E2E test fixtures: extract, don't copy **NEVER copy a full SKILL.md file into an E2E test fixture.** SKILL.md files are @@ -881,6 +926,12 @@ The active skill lives at `~/.claude/skills/gstack/`. After making changes: 2. Fetch and reset in the skill directory: `cd ~/.claude/skills/gstack && git fetch origin && git reset --hard origin/main` 3. Rebuild: `cd ~/.claude/skills/gstack && bun run build` +**If you use gbrain:** the `git reset --hard` in step 2 reverts the brain-aware +(`GBRAIN_CONTEXT_LOAD` / `GBRAIN_SAVE_RESULTS`) blocks that `gstack-config +gbrain-refresh` renders into the install (those generated blocks differ from +`main` by design). After deploying, re-run `gstack-config gbrain-refresh` to +restore them across all your projects' Claude sessions. It's idempotent. + Or copy the binaries directly: - `cp browse/dist/browse ~/.claude/skills/gstack/browse/dist/browse` - `cp design/dist/design ~/.claude/skills/gstack/design/dist/design` @@ -903,6 +954,31 @@ Key routing rules: - Save progress → invoke /context-save - Resume context → invoke /context-restore +## Cross-session decision memory + +Durable decisions and their rationale are captured in an append-only, event-sourced +store at `~/.gstack/projects//decisions.jsonl` so neither you nor the user +re-litigates a settled call or loses the "why" across sessions. This is the reliable, +file-only path: it works with gbrain OFF. (gbrain semantic recall is an optional +enhancement layered on top, never a dependency.) + +- **Resurface** active decisions before re-deciding: `bin/gstack-decision-search` + (`--recent N`, `--scope repo|branch|issue`, `--query KW`, `--all`, `--json`). + Add `--semantic` (with `--query`) to append related hits from gbrain memory when + it's up; it degrades silently to the reliable file results when gbrain is off. + Session start already surfaces scope-relevant active decisions via Context Recovery. + If a decision is listed, treat it as settled with its rationale; if you're about to + reverse it, say so explicitly. +- **Capture** a DURABLE decision when you or the user make one: + `bin/gstack-decision-log '{"decision":"...","rationale":"...","scope":"repo|branch|issue","source":"user|skill|agent","confidence":1-10}'`. + Reverse a prior call with `--supersede `; expunge an accidental secret with + `--redact `; rewrite the log to the active set with `--compact`. Non-interactive + (never prompts), injection-sanitized, and HIGH-secret-blocking on write. +- **Durable means:** architecture choice, scope cut, tool/vendor choice, or a reversal + of a prior call. NOT a turn-level edit, a phrasing tweak, or anything trivially + re-derivable. Capture is curated at the source — log durable decisions only, or the + store becomes noise. + ## GBrain Search Guidance (configured by /sync-gbrain) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e67a307d1..b75d4a898 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,6 +106,22 @@ bun run build bin/dev-teardown ``` +### Brain-aware blocks in a dev workspace (gbrain installed) + +If gbrain is installed and usable (`bin/gstack-gbrain-detect --is-ok` exits 0), +`bin/dev-setup` keeps your tracked `SKILL.md` files canonical and renders the +brain-aware variant (the `GBRAIN_CONTEXT_LOAD` / `GBRAIN_SAVE_RESULTS` blocks) +into `.claude/gstack-rendered/` (gitignored, per-workspace). It then repoints the +workspace's `SKILL.md` symlinks at that render, so your Claude sessions get the +full gbrain experience while `git status` stays clean. Under the hood, dev-setup +passes `GSTACK_SKIP_GBRAIN_REGEN=1` inline to the nested `./setup` (so it never +dirties tracked source) and runs `gen:skill-docs:user --out-dir .claude/gstack-rendered`, +which rewrites only the section-base paths to point at the render. `bin/dev-teardown` +removes the render. To make the blocks live across your *other* projects' Claude +sessions, run `gstack-config gbrain-refresh`, which renders them into the global +install (`~/.claude/skills/gstack`), guarded so it never touches a symlinked or +non-gstack directory. + ## Testing & evals ### Setup @@ -160,6 +176,18 @@ EVALS=1 bun test test/skill-e2e-*.test.ts - Saves full NDJSON transcripts and failure JSON for debugging - Tests live in `test/skill-e2e-*.test.ts` (split by category), runner logic in `test/helpers/session-runner.ts` +**Hermetic by default.** Every E2E runner (claude -p, the real-PTY plan-mode +runner, the Agent SDK runner, plus the codex and gemini runners) spawns its child +through `test/helpers/hermetic-env.ts`: an allowlist-scrubbed environment, a fresh +seeded `CLAUDE_CONFIG_DIR`, a temp `GSTACK_HOME`, and `--strict-mcp-config`. Your +operator `~/.claude` config, MCP servers (gbrain, Conductor), skills, `~/.gstack` +decision logs, and `CONDUCTOR_*` env never leak into the child, so local eval +signal matches CI instead of disagreeing for reasons unrelated to the code under +test. Set `EVALS_HERMETIC=0` to debug against your real operator state (this also +drops `--strict-mcp-config`). The wiring is pinned by `test/hermetic-wiring.test.ts` +(a free static tripwire) and two gate-tier isolation canaries in +`test/skill-e2e-hermetic-canary.test.ts`. + ### E2E observability When E2E tests run, they produce machine-readable artifacts in `~/.gstack-dev/`: @@ -182,6 +210,25 @@ bun run eval:compare # compare two runs — shows per-test deltas + Take bun run eval:summary # aggregate stats + per-test efficiency averages across runs ``` +**Detached runs for agents and long suites.** When an agent (or you, for a run +you don't want to babysit) launches a long eval, use the `eval:bg*` scripts. They +wrap the eval command in `bin/gstack-detach`: a fresh session that escapes a +turn-boundary SIGTERM, a `caffeinate` wrapper that blocks idle-sleep, a machine-wide +`gstack-evals` lock so concurrent worktrees serialize instead of saturating the +model API, a run-scoped log under `~/.gstack-dev/eval-runs/`, a per-tier watchdog, +and a guaranteed `### gstack-detach EXIT= ###` sentinel so a poller never +mistakes silence for success. + +```bash +bun run eval:bg # detached test:evals (diff-based) +bun run eval:bg:all # detached test:evals:all +bun run eval:bg:gate # detached gate-tier suite +bun run eval:bg:periodic # detached periodic-tier suite +``` + +Each prints its log path. Humans running `bun run test:evals` foreground in their +own terminal don't need this — Ctrl-C is intended there. + **Eval comparison commentary:** `eval:compare` generates natural-language Takeaway sections interpreting what changed between runs — flagging regressions, noting improvements, calling out efficiency gains (fewer turns, faster, cheaper), and producing an overall summary. This is driven by `generateCommentary()` in `eval-store.ts`. Artifacts are never cleaned up — they accumulate in `~/.gstack-dev/` for post-mortem debugging and trend analysis. @@ -232,6 +279,14 @@ For template authoring best practices (natural language over bash-isms, dynamic To add a browse command, add it to `browse/src/commands.ts`. To add a snapshot flag, add it to `SNAPSHOT_FLAGS` in `browse/src/snapshot.ts`. Then rebuild. +**Don't bundle puppeteer/Chromium in a skill.** `browse` is the one shared +Chromium per box, including offline local-render workloads. A skill that needs to +rasterize its own HTML/JSON (diagrams, cards, og-images) should route through +`browse` — `screenshot --selector` for visual output, `load-html` + `js --out` for +bytes a render function returns — instead of `npm i puppeteer` and downloading a +second Chromium that drifts out of version sync. One install to pin, one daemon to +manage. + ## Jargon list (V1 writing style) gstack's Writing Style section (injected into every tier-≥2 skill's preamble) @@ -326,8 +381,8 @@ If you're using [Conductor](https://conductor.build) to run multiple Claude Code | Hook | Script | What it does | |------|--------|-------------| -| `setup` | `bin/dev-setup` | Copies `.env` from main worktree, installs deps, symlinks skills, runs `./setup` non-interactively | -| `archive` | `bin/dev-teardown` | Removes skill symlinks, cleans up `.claude/` directory | +| `setup` | `bin/dev-setup` | Copies `.env` from main worktree, installs deps, symlinks skills, runs `./setup` non-interactively, and (if gbrain is installed) renders brain-aware blocks into `.claude/gstack-rendered/` without dirtying tracked source | +| `archive` | `bin/dev-teardown` | Removes skill symlinks, the `.claude/gstack-rendered/` render, and cleans up `.claude/` directory | When Conductor creates a new workspace, `bin/dev-setup` runs automatically. It detects the main worktree (via `git worktree list`), copies your `.env` so API keys carry over, and sets up dev mode — no manual steps needed. diff --git a/ETHOS.md b/ETHOS.md index a04cd9d1c..3dbd5e570 100644 --- a/ETHOS.md +++ b/ETHOS.md @@ -31,16 +31,21 @@ The last 10% of completeness that teams used to skip? It costs seconds now. --- -## 1. Boil the Lake +## 1. Boil the Ocean -AI-assisted coding makes the marginal cost of completeness near-zero. When -the complete implementation costs minutes more than the shortcut — do the +"Don't boil the ocean" was the right advice when engineering time was the +bottleneck. That era is over. AI-assisted coding makes the marginal cost of +completeness near-zero, so the old caution has quietly turned into an excuse. +When the complete implementation costs minutes more than the shortcut — do the complete thing. Every time. -**Lake vs. ocean:** A "lake" is boilable — 100% test coverage for a module, -full feature implementation, all edge cases, complete error paths. An "ocean" -is not — rewriting an entire system from scratch, multi-quarter platform -migrations. Boil lakes. Flag oceans as out of scope. +**Ocean, lakes first:** The ocean is the destination — 100% test coverage for a +module, full feature implementation, all edge cases, complete error paths. You +get there one lake at a time: each lake is a boilable unit, not the ceiling. +"That's boiling the ocean" is no longer a reason to ship a shortcut — boiling +the ocean is the goal. The only thing still out of scope is genuinely unrelated +work: a multi-quarter platform migration that has nothing to do with the task at +hand. Flag that as separate scope. Boil everything else. **Completeness is cheap.** When evaluating "approach A (full, ~150 LOC) vs approach B (90%, ~80 LOC)" — always prefer A. The 70-line delta costs @@ -144,7 +149,7 @@ think it's better, state what context you might be missing, and ask. Never act. ## How They Work Together -Boil the Lake says: **do the complete thing.** +Boil the Ocean says: **do the complete thing.** Search Before Building says: **know what exists before you decide what to build.** Together: search first, then build the complete version of the right thing. diff --git a/README.md b/README.md index a0d9c40e3..4bb177c3a 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,8 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan- | `/autoplan` | **Review Pipeline** | One command, fully reviewed plan. Runs CEO → design → eng review automatically with encoded decision principles. Surfaces only taste decisions for your approval. | | `/spec` | **Spec Author** | Turn vague intent into a precise, executable spec in five phases (why, scope, technical with mandatory code-reading, draft, file). Codex quality gate before file (blocks below 7/10), fail-closed secret redaction, dedupe against existing issues, archive to `$GSTACK_STATE_ROOT/projects/$SLUG/specs/` for team-corpus recall. `--execute` spawns `claude -p` in a fresh worktree; `/ship` auto-closes the source issue on merge. Plan-mode aware. | | `/learn` | **Memory** | Manage what gstack learned across sessions. Review, search, prune, and export project-specific patterns, pitfalls, and preferences. Learnings compound across sessions so gstack gets smarter on your codebase over time. | +| `/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. | ### Which review should I use? @@ -429,7 +431,8 @@ Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guid | Doc | What it covers | |-----|---------------| | [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) | -| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Lake, Search Before Building, three layers of knowledge | +| [Diagrams & Document Formats](docs/howto-diagrams-and-formats.md) | Mermaid/excalidraw fences in PDFs, image sizing and safety defaults, `--to html\|docx`, `/diagram` triplets | +| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Ocean, Search Before Building, three layers of knowledge | | [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 | diff --git a/SKILL.md b/SKILL.md index 60405f27d..90774950e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -45,6 +45,16 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX" source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true REPO_MODE=${REPO_MODE:-unknown} echo "REPO_MODE: $REPO_MODE" +_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive") +case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac +echo "SESSION_KIND: $_SESSION_KIND" +# Conductor host: AskUserQuestion is unreliable here (native disabled, MCP +# variant flaky), so skills render decisions as prose instead of calling the +# tool. Gated on !headless so an eval/CI run INSIDE Conductor (GSTACK_HEADLESS) +# still BLOCKs rather than rendering prose to nobody. +if [ "$_SESSION_KIND" != "headless" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then + echo "CONDUCTOR_SESSION: true" +fi _LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no") echo "LAKE_INTRO: $_LAKE_SEEN" _TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true) @@ -124,7 +134,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co ## Skill Invocation During Plan Mode -If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no variant is callable, the skill is BLOCKED — stop and report `BLOCKED — AskUserQuestion unavailable` per the AskUserQuestion Format rule. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. +If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?" @@ -159,7 +169,7 @@ touch ~/.gstack/.writing-style-prompted Skip if `WRITING_STYLE_PENDING` is `no`. -If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open: +If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open: ```bash open https://garryslist.org/posts/boil-the-ocean @@ -914,10 +924,10 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. | `cookies` | All cookies as JSON | | `css ` | Computed CSS value | | `dialog [--clear]` | Dialog messages | -| `eval ` | Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. | +| `eval [--out ] [--raw]` | Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. With --out , the result is written to disk (base64 data URL decoded to bytes unless --raw); --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel). | | `inspect [selector] [--all] [--history]` | Deep CSS inspection via CDP — full rule cascade, box model, computed styles | | `is ` | State check on element. Valid values: visible, hidden, enabled, disabled, checked, editable, focused (case-sensitive). accepts a CSS selector OR an @ref token from a prior snapshot (e.g. @e3, @c1) — refs are interchangeable with selectors anywhere a selector is expected. | -| `js ` | Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. | +| `js [--out ] [--raw]` | Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. With --out , the result is written to disk instead of returned (a base64 data URL is decoded to raw bytes unless --raw is given) — ideal for rasterizing local renders to PNG without serializing megabytes back through the CLI. --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel). | | `network [--clear]` | Network requests | | `perf` | Page load timings | | `storage | storage set ` | Read both localStorage and sessionStorage as JSON. With "set ", write to localStorage only (sessionStorage is read-only via this command — set it with `js sessionStorage.setItem(...)`). | diff --git a/TODOS.md b/TODOS.md index 93b24c446..80660c3e6 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,5 +1,48 @@ # TODOS +## NEXT PRIORITY + +### P1: #1882 — portable skill-install prefix (non-`gstack` install dirs break silently) + +**What:** Every generated SKILL.md hardcodes the literal `~/.claude/skills/gstack/...` +for its `bin/`/asset calls (the per-invocation telemetry/config preamble plus ~9 +resolvers). `setup` wires the top-level skill symlinks for any directory name, so +installing at `~/.claude/skills/` leaves every internal `bin` reference +pointing at a non-existent `~/.claude/skills/gstack/` path — failing **silently, at +skill-invocation time**. Make the emitted references portable: resolve the install +root at runtime (the preamble already defines `GSTACK_ROOT`/`GSTACK_BIN` in +`scripts/resolvers/preamble/generate-preamble-bash.ts` but the literals don't use +them) and emit `$GSTACK_BIN`-relative paths instead of the hardcoded prefix. + +**Why:** Filed as #1882. Split out of the June 2026 fix wave (decision A) once +implementation showed it is a host-config/design change, not a fix-wave patch. The +urgent half — the guard/freeze/careful frontmatter hooks broken on CC 2.1.162 — was +already fixed in that wave (#1871) with a literal `$HOME`-anchored path, because +frontmatter hooks run before any runtime variable exists and cannot use `$GSTACK_BIN`. +So #1882 is now purely the body-preamble portability work. + +**Pros:** Unblocks installs at any directory name; removes a whole class of silent +invocation-time failures. +**Cons:** Touches the most load-bearing bash in the repo (every skill's preamble); +a silent mistake breaks all 52 skills. High blast radius — needs its own focused PR. + +**Context / where to start:** +- Rewire `ctx.paths.binDir` (and browse/design dir paths) + the ~9 resolvers that + emit the literal (`testing.ts`, `review.ts`, `design.ts`, `browse.ts`, + `redact-doc.ts`, `tasks-section.ts`, `preamble/generate-*.ts`) to use the + preamble-defined `$GSTACK_ROOT`/`$GSTACK_BIN`. +- Ensure `GSTACK_ROOT`/`GSTACK_BIN` are defined before first use in EVERY skill's + preamble (verify the telemetry preamble's first bin call is after the definition). +- **Test conflict (verified):** `test/gen-skill-docs.test.ts:1942` and the sibling + ship assertion currently *assert* generated Claude output `.toContain('~/.claude/skills/gstack')` + as a guardrail that Codex-host paths don't leak. These must be rewritten to match + the new portable scheme. +- Regenerate all 52 SKILL.md (`bun run scripts/gen-skill-docs.ts --host all`); never + hand-edit generated files. Bisect: resolver/host-config change commit, then the + 52-file regen commit. +- Smoke-test a skill invocation from a non-`gstack` install dir to prove the fix. +- Sibling of #349 (the `$CLAUDE_CONFIG_DIR` / `~/.claude` path issue). + ## Test infrastructure ### ✅ DONE (v1.53.1.0): Rebaseline parity-suite (v1.44.1 → v1.53.0.0) @@ -1951,7 +1994,7 @@ Shipped in v0.6.5. TemplateContext in gen-skill-docs.ts bakes skill name into pr **What:** Write a postinstall script that patches Playwright's CDP layer to suppress `Runtime.enable` and use `addBinding` for context ID discovery, same approach as rebrowser-patches. Eliminates the `navigator.webdriver`, `cdc_` markers, and other CDP artifacts that sites like Google use to detect automation. -**Why:** Our current stealth narrows to `navigator.webdriver` masking + ChromeDriver `cdc_` runtime cleanup + Permissions API patch (v1.28.0.0 narrowed it from also faking plugins/languages, since modern fingerprinters punish inconsistent fakes more than they punish admitted defaults). That's enough for most sites but Google still triggers captchas, because the real detection is at the CDP protocol level. rebrowser-patches proved the approach works but their patches target Playwright 1.52.0 and don't apply to our 1.58.2. We need our own patcher using string matching instead of line-number diffs. 6 files, ~200 lines of patches total. +**Why:** As of v1.58.3.0 our JS-layer stealth is "Layer C" — always-on `navigator.webdriver` mask + `window.chrome.*` shape + `Notification.permission`/Permissions alignment + per-install `hardwareConcurrency`/`deviceMemory` + a `Function.prototype.toString` proxy + an automation-global sweep + ChromeDriver `cdc_`/`__webdriver` cleanup (still NOT faking plugins/languages, since modern fingerprinters punish inconsistent fakes more than they punish admitted defaults). That closes most JS-observable tells, but Google still triggers captchas because the deepest detection is at the CDP protocol level, which a page-world init script can't reach. rebrowser-patches proved the CDP approach works but their patches target Playwright 1.52.0 and don't apply to our 1.58.2. We need our own patcher using string matching instead of line-number diffs. 6 files, ~200 lines of patches total. (Layer C's toString proxy still has descriptor/Reflect.ownKeys surfaces; pushing the spoofs to native code via CDP suppression or the Chromium fork makes the JS layer obsolete.) **Context:** Full analysis of rebrowser-patches source: patches 6 files in `playwright-core/lib/server/` (crConnection.js, crDevTools.js, crPage.js, crServiceWorker.js, frames.js, page.js). Key technique: suppress `Runtime.enable` (the main CDP detection vector), use `Runtime.addBinding` + `CustomEvent` trick to discover execution context IDs without it. Our extension communicates via Chrome extension APIs, not CDP Runtime, so it should be unaffected. Write E2E tests that verify: (1) extension still loads and connects, (2) Google.com loads without captcha, (3) sidebar chat still works. @@ -2283,3 +2326,92 @@ into `test/helpers/fake-gbrain.ts` when the second consumer arrives runs). **Depends on:** None. + +### P2: Real-session carve canary (E3, deferred from carve-guard plan) + +**What:** Wire a real-session section-Read-miss canary on top of the +carved skills. When a real user session drives a carved skill and the +agent does NOT Read a section the skeleton's STOP directive pointed it +at, log it (salted, content-free) to +`~/.gstack/analytics/section-reads.jsonl` and surface drift via +`bun run eval:summary`. Non-blocking alert, never a merge gate +(real-session data is non-deterministic). + +**Why:** The static (E2) + behavioral (T2) guards prove carves are +structurally sound and that a real agent Reads sections in a controlled +eval. They do NOT see production drift — a prompt-context change that +makes live agents start skipping a section. The canary is the only +mechanism that catches that, from real usage. + +**Context:** Deferred from the carve-guard-hardening plan (D5→T2, codex +outside-voice #7). `test/helpers/transcript-section-logger.ts` exists but +is built for deterministic test transcripts + ship action fingerprints, +NOT real-session drift — it needs rework before it can back this. Ship +the deterministic guards first; add this once they've proven useful. The +carved-skill set + each skill's `requiredReads` are already declared in +`test/helpers/carve-guards.ts`, so the canary reads its expectations +from there. + +**Effort:** M (human ~2d, CC ~4h). + +**Depends on:** `transcript-section-logger.ts` real-session-drift rework. + +### P2: Harden behavioral section-loading test hermeticity + +**What:** `captureSectionReads` in `test/helpers/auq-sdk-capture.ts` accepts ANY +Read whose path matches `sections/.md`. The skeleton's STOP-Read directive +points at the gstack-root install path (`scripts/resolvers/sections.ts` builds it +from `ctx.paths.skillRoot`), not the planted fixture copy. So a run can satisfy +the section-read assertion by reading the GLOBAL install's section instead of the +hermetic fixture. + +**Why:** A behavioral test that passes by reading the global install doesn't prove +THIS branch's carved section loads. If the fixture's section were broken but the +global install's weren't, the test would still pass. + +**Context:** Codex outside-voice finding on the carve-guard ship (v1.57.0.0). +Pre-existing in `auq-sdk-capture.ts` — affects `skill-e2e-ship-section-loading`, +`skill-e2e-plan-ceo-review-section-loading`, and the new +`carve-section-loading.test.ts`. Fix: match the fixture's ABSOLUTE sections path +(the `planDir` copy), not a bare `sections/.md` regex; or rewrite the STOP +path to the fixture during the run. + +**Effort:** S (human ~3h, CC ~30min). **Depends on:** None. + +### P3: Content-hash diagram render cache for make-pdf + +**What:** Cache rendered diagram SVG/PNG in `~/.gstack/cache/diagram-render/`, +keyed on `sha256(fence source + bundle version + render options)`, so repeat +`make-pdf` runs skip the browse render tab for unchanged diagrams. + +**Why:** Every run currently re-renders every fence (~150-300ms each). Docs with +10+ diagrams pay seconds per iteration during write-preview loops. Codex +outside-voice flagged the missing cache story during the eng review of the +diagram engine plan (2026-06-11, D7). + +**Context:** The diagram-render bundle ships a `BUILD_INFO.json` with a content +hash (see `lib/diagram-render/`) — use that as the bundle-version cache key +component so bundle bumps invalidate cleanly. Invalidation surface is the main +risk: stale renders after a mermaid theme change must not survive. Only worth +building once users hit multi-diagram docs; wedge perf is fine without it. + +**Effort:** S (human ~1d, CC ~30min). **Depends on:** diagram engine wedge +shipping (lib/diagram-render bundle versioning). + +### P3: Dedupe the make-pdf e2e gate-test harness + +**What:** Five e2e files (`combined-gate`, `emoji-gate`, `diagram-gate`, +`landscape-gate`, `format-gate`) each hand-roll the same prerequisite probe +(binary/browse/poppler checks with CI hard-fail vs local skip), mkdtemp/rm +lifecycle, and child-timeout constants. Extract a shared +`make-pdf/test/e2e/helpers.ts` (prerequisites(), withWorkDir(), runGenerate()). + +**Why:** Review-army maintainability finding on v1.58.0.0 — the boilerplate +diverges a little more with each new gate (diagram-gate now captures stderr +via Bun.spawnSync while the others use execFileSync), and a future fix to the +CI-hard-fail contract has to land five times. + +**Context:** Deferred at ship time (D8.2) because it's test-only churn across +five green files at the tail of a release. Zero user-facing value; pure DRY. + +**Effort:** S (human ~3h, CC ~20min). **Depends on:** None. diff --git a/VERSION b/VERSION index 1d97cf7aa..32e0f507a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.56.0.0 +1.58.3.0 diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 5d5f6334c..49db38ff9 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -54,6 +54,16 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX" source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true REPO_MODE=${REPO_MODE:-unknown} echo "REPO_MODE: $REPO_MODE" +_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive") +case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac +echo "SESSION_KIND: $_SESSION_KIND" +# Conductor host: AskUserQuestion is unreliable here (native disabled, MCP +# variant flaky), so skills render decisions as prose instead of calling the +# tool. Gated on !headless so an eval/CI run INSIDE Conductor (GSTACK_HEADLESS) +# still BLOCKs rather than rendering prose to nobody. +if [ "$_SESSION_KIND" != "headless" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then + echo "CONDUCTOR_SESSION: true" +fi _LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no") echo "LAKE_INTRO: $_LAKE_SEEN" _TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true) @@ -133,7 +143,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co ## Skill Invocation During Plan Mode -If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no variant is callable, the skill is BLOCKED — stop and report `BLOCKED — AskUserQuestion unavailable` per the AskUserQuestion Format rule. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. +If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?" @@ -168,7 +178,7 @@ touch ~/.gstack/.writing-style-prompted Skip if `WRITING_STYLE_PENDING` is `no`. -If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open: +If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open: ```bash open https://garryslist.org/posts/boil-the-ocean @@ -303,13 +313,39 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions: "AskUserQuestion" can resolve to two tools at runtime: the **host MCP variant** (e.g. `mcp__conductor__AskUserQuestion` — appears in your tool list when the host registers it) or the **native** Claude Code tool. -**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies. +**Conductor rule (read before the MCP rule):** if `CONDUCTOR_SESSION: true` was echoed by the preamble, do NOT call AskUserQuestion at all — neither native nor any `mcp__*__AskUserQuestion` variant. Render EVERY decision brief as the **prose form** below and STOP. This is proactive, not a reaction to a failure: Conductor disables native AUQ and its MCP variant is flaky (it returns `[Tool result missing due to internal error]`), so prose is the reliable path. **Auto-decide preferences still apply first:** if a `[plan-tune auto-decide]