diff --git a/.github/workflows/windows-scoped-tests.yml b/.github/workflows/windows-scoped-tests.yml new file mode 100644 index 000000000..1675d15e7 --- /dev/null +++ b/.github/workflows/windows-scoped-tests.yml @@ -0,0 +1,206 @@ +name: Windows Scoped Tests + +# The path-scoped Windows free shard (ship-90-90 plan D). windows-smoke.yml is +# still the required Windows check on every pull request, and the full +# curated suite (windows-free-tests.yml) still runs nightly on main. This +# workflow sits between them: when a pull request touches path, file-name or +# process code, one windows-latest job runs the test files the pull request +# changed that are in the curated Windows-safe subset. +# +# "Path, file-name or process code" (the windows-scope filter step): +# - file names: anything under a bin/ directory (shell entry points and +# skill hooks such as freeze/bin/check-freeze.sh, #3758), shell and +# PowerShell scripts, hosts/*/hooks, setup, bun.lock, the Windows +# curation (scripts/test-free-shards.ts, scripts/lib/windows-curation.ts, +# the Windows duration seed, windows-free-tests.yml), and code files whose +# names say path, spawn, exec, process, shell, launch, win32, windows, +# tmp, temp, symlink, state-root, slug, claude-bin, bun-isolation or +# script-invocation; +# - added or removed code lines that spawn processes or spell paths: +# child_process, spawn/exec calls, Bun.spawn, Bun.$, path.win32 and the +# other separator-sensitive path calls, process.platform/execPath, win32, +# tmpdir(), /dev/null, /tmp/, symlink, realpath, chmod, cygpath, +# USERPROFILE, homedir(), fileURLToPath and .exe. #3829's Windows break was +# one such line in a test it added: spawnSync(process.execPath, [..., +# '--config=/dev/null']), which native Windows Bun cannot open. +# test/windows-path-scope-workflow.test.ts proves the filter catches #3758 +# and #3829 and passes over a docs-only and a pure-logic change. +# +# The windows-scoped-tests job is the check to read: it passes when nothing +# is in scope (no Windows job ran) and otherwise requires the Windows job, so +# a skipped run reports success. It is not one of the required checks, but +# gstack-ship-queue ready and land refuse a head with any failed check, so a +# red Windows job blocks the pull request it runs on: in a pull request that +# touches path, file-name or process code, a changed curated test file has to +# pass on Windows. A change to path or process code with no changed curated +# test selects nothing; the nightly suite covers it. +# gstack-pregate reads a job keyed on a dispatch input as manual, never a PR +# obligation: this check runs on the pull request itself. A workflow_dispatch +# run with the files input reruns chosen Windows-safe files on a branch. + +on: + pull_request: + branches: [main] + workflow_dispatch: + inputs: + files: + description: Space-separated Windows-safe free test files to run (from bun run test:windows --list) + type: string + required: true + +concurrency: + group: windows-scoped-tests-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +# Test-only lane — no token writes. +permissions: + contents: read + +jobs: + windows-scope: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + files: ${{ steps.select.outputs.files }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: github.event_name == 'pull_request' + with: + fetch-depth: 2 + persist-credentials: false + - id: filter + name: Does the diff touch path, file-name or process code? + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + git diff --name-only --no-renames HEAD^1 HEAD > "$RUNNER_TEMP/changed-files" + code_re='\.(ts|tsx|js|mjs|cjs|sh|bash|ps1|cmd|bat)$|(^|/)bin/[^/.]+$|(^|/)setup$' + path_re='(^|/)bin/|\.(sh|bash|ps1|cmd|bat)$|(^|/)hosts/[^/]+/hooks/|(^|/)setup$|^bun\.lock$|^scripts/test-free-shards\.ts$|^scripts/lib/windows-curation\.ts$|^scripts/free-test-durations-windows\.json$|^\.github/workflows/windows-free-tests\.yml$' + name_re='(path|spawn|exec|process|shell|launch|win32|windows|tmp|temp|symlink|state-root|slug|claude-bin|bun-isolation|script-invocation)[^/]*$' + content_re='child_process|\b(spawn|spawnSync|execSync|execFile|execFileSync)[[:space:]]*\(|Bun\.(spawn|spawnSync|\$)|\bpath\.(win32|posix|sep|delimiter|relative|isAbsolute|normalize)\b|process\.(platform|execPath)|\bwin32\b|tmpdir\(|/dev/null|/tmp/|symlink|realpath|chmod|cygpath|USERPROFILE|homedir\(|fileURLToPath|\.exe\b' + { + grep -E "$path_re" "$RUNNER_TEMP/changed-files" | sed 's/^/path: /' || true + grep -E "$code_re" "$RUNNER_TEMP/changed-files" | grep -E "$name_re" | sed 's/^/name: /' || true + { grep -E "$code_re" "$RUNNER_TEMP/changed-files" || true; } | while IFS= read -r file; do + git diff -U0 --no-renames HEAD^1 HEAD -- "$file" | grep -E '^[+-]' | grep -vE '^(\+\+\+|---) ' | grep -m1 -E "$content_re" | awk -v f="$file" '{ print "content: " f ": " $0 }' || true + done + } > "$RUNNER_TEMP/windows-scope" + if [ -s "$RUNNER_TEMP/windows-scope" ]; then + echo 'windows=true' >> "$GITHUB_OUTPUT" + { echo 'This pull request touches path, file-name or process code:'; echo '~~~'; head -n 40 "$RUNNER_TEMP/windows-scope"; echo '~~~'; } >> "$GITHUB_STEP_SUMMARY" + else + echo 'windows=false' >> "$GITHUB_OUTPUT" + echo 'This pull request touches no path, file-name or process code, so no Windows job runs. windows-smoke still runs on every pull request.' >> "$GITHUB_STEP_SUMMARY" + fi + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + if: steps.filter.outputs.windows == 'true' + with: + bun-version: 1.4.2 + - id: select + name: Pick the changed test files in the curated Windows-safe subset + if: steps.filter.outputs.windows == 'true' + run: | + set -euo pipefail + GITHUB_STEP_SUMMARY=/dev/null bun run scripts/test-free-shards.ts --windows-only --ci-plan "$RUNNER_TEMP/curated.json" --shards 1 > /dev/null + jq -r '.shards[].files[]' "$RUNNER_TEMP/curated.json" | LC_ALL=C sort -u > "$RUNNER_TEMP/curated" + LC_ALL=C sort -u "$RUNNER_TEMP/changed-files" | LC_ALL=C comm -12 - "$RUNNER_TEMP/curated" > "$RUNNER_TEMP/selected" + if [ ! -s "$RUNNER_TEMP/selected" ]; then + echo 'No changed test file is in the curated Windows-safe subset, so no Windows job runs.' >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + { echo 'files<> "$GITHUB_OUTPUT" + { echo "The Windows job runs $(wc -l < "$RUNNER_TEMP/selected" | tr -d ' ') changed Windows-safe test file(s):"; sed 's/^/- /' "$RUNNER_TEMP/selected"; } >> "$GITHUB_STEP_SUMMARY" + + windows-scoped-shard: + needs: windows-scope + if: ${{ (github.event_name == 'workflow_dispatch' && inputs.files != '') || needs.windows-scope.outputs.files != '' }} + runs-on: windows-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24.18.0 + + # The same inputs as a windows-free-tests.yml shard, with no bun install + # cache (a cold install beats restoring it on windows-latest). + - name: Configure git identity (required by tests that init temp repos) + run: | + git config --global user.email "windows-ci@gstack.test" + git config --global user.name "Windows CI" + git config --global init.defaultBranch main + shell: bash + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build server-node.mjs (required by Windows browse path) + run: bash browse/scripts/build-node-server.sh + shell: bash + + - name: Generate host SKILL.md outputs (.agents, .factory) + run: bun run gen:skill-docs --host all + shell: bash + + - name: Install Chromium for the Node worker smoke + run: bunx playwright install chromium + + - name: Run the scoped Windows-safe test files + env: + FILES: ${{ inputs.files || needs.windows-scope.outputs.files }} + GSTACK_FREE_RETRY_FLAKY: '1' + GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} + run: | + set -euo pipefail + printf '%s\n' "$FILES" | tr -d '\r' | tr ' ' '\n' | sed '/^$/d' > "$RUNNER_TEMP/windows-scoped-files.txt" + bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-scoped-files.txt" + shell: bash + + - name: Upload full test logs + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: windows-scoped-test-logs + path: | + .context/free-test-logs/gstack-free-test-*.log + ${{ runner.temp }}/gstack-free-test-*.log + include-hidden-files: true + if-no-files-found: ignore + + - name: Upload flake ledger + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: flake-ledger-windows-scoped + path: ${{ runner.temp }}/flake-ledger.jsonl + if-no-files-found: ignore + retention-days: 90 + + windows-scoped-tests: + needs: [windows-scope, windows-scoped-shard] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Pass when nothing is in scope, else require the Windows job + env: + SCOPE_RESULT: ${{ needs.windows-scope.result }} + SHARD_RESULT: ${{ needs.windows-scoped-shard.result }} + FILES: ${{ inputs.files || needs.windows-scope.outputs.files }} + run: | + set -eu + test "$SCOPE_RESULT" = success + if [ -z "$FILES" ]; then + echo 'Nothing in scope (no changed Windows-safe test file in a pull request that touches path, file-name or process code); passing without a Windows run.' >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + test "$SHARD_RESULT" = success diff --git a/.github/workflows/windows-setup-e2e.yml b/.github/workflows/windows-setup-e2e.yml index 2ca74e7b4..37cee929a 100644 --- a/.github/workflows/windows-setup-e2e.yml +++ b/.github/workflows/windows-setup-e2e.yml @@ -32,6 +32,13 @@ name: Windows Setup E2E # Separate from windows-free-tests.yml because that one runs a curated # unit-test subset; this one exercises the install path itself. # +# Runs by path (ship-90-90 plan D): the paths below are the install files +# this workflow runs or ./setup sources or runs. The windows-scope job then +# drops a pull request whose only install change is package.json's version +# line, which is what the merge-train stamp commit puts on every PR head. +# windows-scope's list must match the paths list (pinned by +# test/windows-path-scope-workflow.test.ts). +# # Runner: GitHub-hosted free windows-latest, two parallel jobs. ~10-25 min. on: @@ -39,6 +46,7 @@ on: branches: [main] paths: - 'package.json' + - 'bun.lock' - 'scripts/build.sh' - 'scripts/build-cso.sh' - 'scripts/build-cso-windows.ps1' @@ -64,6 +72,18 @@ on: - 'bin/gstack-claude-root.sh' - 'bin/gstack-browser-ensure' - 'bin/gstack-path-spelling.sh' + - 'bin/gstack-config' + - 'bin/gstack-patch-names' + - 'bin/gstack-state-root.sh' + - 'bin/gstack-skill-name.sh' + - 'bin/gstack-install-registry.sh' + - 'bin/gstack-render-claude.sh' + - 'bin/gstack-host-renders.sh' + - 'bin/gstack-migrate-claude-code' + - 'bin/gstack-settings-hook' + - 'bin/gstack-relink' + - 'bin/gstack-harness' + - 'scripts/preflight-codex-overlap.ts' - 'lib/path-spelling.ts' - 'hosts/claude/hooks/**' - 'gstack-upgrade/**' @@ -81,7 +101,94 @@ permissions: contents: read jobs: + windows-scope: + # The paths filter above starts this workflow; this job decides whether + # the Windows jobs run. A workflow_dispatch run always runs them. + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + install: ${{ steps.scope.outputs.install }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: github.event_name == 'pull_request' + with: + fetch-depth: 2 + persist-credentials: false + - id: scope + name: Run unless the only install change is package.json's version line + run: | + set -euo pipefail + if [ "$GITHUB_EVENT_NAME" != pull_request ]; then + echo 'install=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + pkg_version_only=true + if git diff -U0 --no-renames HEAD^1 HEAD -- package.json | grep -E '^[+-]' | grep -vE '^(\+\+\+|---) ' \ + | grep -vE '^[+-] "version": "[^"]*",?$' > /dev/null; then + pkg_version_only=false + fi + git diff --name-only --no-renames HEAD^1 HEAD | while IFS= read -r file; do + if [ "$file" = package.json ] && [ "$pkg_version_only" = true ]; then continue; fi + while IFS= read -r glob; do + # shellcheck disable=SC2254 + case "$file" in ${glob//\*\*/*}) echo "$file"; break ;; esac + done <<'PATHS' + package.json + bun.lock + scripts/build.sh + scripts/build-cso.sh + scripts/build-cso-windows.ps1 + scripts/write-version-files.sh + setup + setup.ps1 + test/setup-ps1.test.ts + docs/troubleshooting.md + browse/src/cli.ts + browse/src/find-browse.ts + browse/src/server.ts + browse/src/meta-commands.ts + browse/scripts/build-node-server.sh + design/src/cli.ts + make-pdf/src/cli.ts + bin/gstack-global-discover.ts + bin/gstack-paths + bin/gstack-doctor + bin/gstack-launch-probe.sh + bin/gstack-hook-check + bin/gstack-session-update + bin/gstack-ff-update + bin/gstack-claude-root.sh + bin/gstack-browser-ensure + bin/gstack-path-spelling.sh + bin/gstack-config + bin/gstack-patch-names + bin/gstack-state-root.sh + bin/gstack-skill-name.sh + bin/gstack-install-registry.sh + bin/gstack-render-claude.sh + bin/gstack-host-renders.sh + bin/gstack-migrate-claude-code + bin/gstack-settings-hook + bin/gstack-relink + bin/gstack-harness + scripts/preflight-codex-overlap.ts + lib/path-spelling.ts + hosts/claude/hooks/** + gstack-upgrade/** + .github/workflows/windows-setup-e2e.yml + PATHS + done > "$RUNNER_TEMP/install-files" + if [ -s "$RUNNER_TEMP/install-files" ]; then + echo 'install=true' >> "$GITHUB_OUTPUT" + { echo 'Install files changed, so the Windows jobs run:'; sed 's/^/- /' "$RUNNER_TEMP/install-files"; } >> "$GITHUB_STEP_SUMMARY" + else + echo 'install=false' >> "$GITHUB_OUTPUT" + echo "The only install change is package.json's version line (the merge-train stamp), so the Windows jobs do not run." >> "$GITHUB_STEP_SUMMARY" + fi + windows-setup: + needs: windows-scope + if: needs.windows-scope.outputs.install == 'true' runs-on: windows-latest timeout-minutes: 35 @@ -377,6 +484,8 @@ jobs: # docs/troubleshooting.md, against the runner's real ~/.claude install # path ($HOME in PowerShell). GSTACK_E2E_POWERSHELL and # GSTACK_E2E_UPDATER_PATH_PREFIX exist only to run these steps off Windows. + needs: windows-scope + if: needs.windows-scope.outputs.install == 'true' runs-on: windows-latest timeout-minutes: 50 env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 945e76db2..c432700aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,86 @@ # Changelog +## [1.94.10.0] - 2026-11-03 + +**Windows checks now run by path.** Windows Setup E2E, the 10-to-25-minute install test, no longer starts its Windows jobs on a pull request whose only install change is `package.json`'s version line, which the merge-train stamp puts on every pull request before it merges. Of the 61 pull requests in its last 100 runs (as of 2026-11-03), 37 changed no install file besides `package.json`. Its path list also gains `bun.lock` and the `bin/` scripts that the test or `./setup` runs. A new `Windows Scoped Tests` workflow runs the Windows-safe test files a pull request changed, on one Windows job, when the pull request touches path, file-name or process code, such as a hook script or a new `spawnSync` call. On the last 120 commits to main (through 923fc0a) it would have run on 50, with a median of one test file. It catches both Windows breaks the plan names: /freeze's path check (#3758) and a test that started Bun with `--config=/dev/null` (#3829). A red run blocks its pull request like any failed check, so this release also fixes #3829's test, which has failed every nightly Windows run since it landed. `windows-smoke` still runs on every pull request, and the full Windows suite still runs nightly. + +### CI +- `.github/workflows/windows-setup-e2e.yml`: a Linux `windows-scope` job runs first and skips both Windows jobs when the only install change is `package.json`'s `"version"` line. Paths add `bun.lock`, `bin/gstack-config`, `bin/gstack-patch-names` and the scripts `./setup` sources or runs. +- `.github/workflows/windows-scoped-tests.yml` (new): a Linux scope job reads the diff's file names and changed code lines, picks the changed files in the curated Windows-safe subset, and a `windows-latest` job runs them. The `windows-scoped-tests` check passes when nothing is in scope, so a skipped run still reports success. A `workflow_dispatch` with a `files` input reruns chosen Windows-safe files on a branch. + +### Tests +- `test/review-risk-linear.test.ts`: the rules-hash case starts Bun with `bunIsolationArgs()` (`--config=NUL` on native Windows) instead of `--config=/dev/null`, which native Windows Bun cannot open (nightly Windows Free Tests run 41420490350, EINVAL). +- `scripts/test-pr-profile.ts`: the new workflow is listed as free-lane only beside the other Windows workflows (test/free-fixtures.test.ts requires every workflow to be classified). +- `test/windows-path-scope-workflow.test.ts`: runs both scope steps in a two-commit git repository. The filter catches #3758's and #3829's changed files and passes over a docs-and-stamp change and a pure-logic change. The Setup E2E scope skips a version-only `package.json` bump and runs on a dependency change. `windows-smoke` stays unfiltered, and no job that can skip is a required check. + +## [1.94.9.0] - 2026-11-03 + +**A pull request can clear its remote gate obligation from a run with named flaky failures, once the owner grants it (GST-1096).** A timing flake in a test file the pull request never touched used to keep the pregate red with no way through but another paid run. `gstack-pregate clear --remote --run --flaky-exception "::" --pr --grant ` now accepts such a run when every failure is named from the run's own shard results and logs, at most three cases, each file outside the pull request's diff and its import closure, none paid and none a security scan test, and a gstack-approvals row in the owner's words binds the repository, PR, head, tree, lane, run attempt and cases. It covers the pregate only, and a red required CI check still needs its own green run. + +### Added +- **`--flaky-exception`, `--pr` and `--grant` on `gstack-pregate clear`** (`lib/pregate/flaky-exception.ts`). Every job of the run attempt is reconciled (only test-shard jobs and their aggregate may fail), every planned shard needs a complete result, and each failed shard's log must name no failure its result leaves out. Without `--grant` the clear checks everything and prints the exact `gstack-approvals add` command, whose scope is a digest of everything the grant binds, so a grant can never be moved to another head or case list. +- **`pass-with-exception`.** pregate.json records `exceptions`; `gstack-pregate verify` re-checks the binding offline and prints `PREGATE: pass-with-exception (; grant )`; `gstack-ship-queue ready` accepts it and prints a `FLAKY_EXCEPTION:` line; the ship receipt records `flaky_exception`. +- **docs/pregate.md#flaky-exception**, including why `base_red` is not used: it proves a failure by reproducing it on the base, and a timing flake passes there. + +### Tests +- `test/pregate-flaky-exception.test.ts`: a red run with one named flake clears once the bound grant exists, and verify, ready and the receipt report it; refusals for an unnamed or extra failure, a case that did not fail, more than three cases, an unattributed failure, a failed non-shard job, a missing result or log, a timed-out shard, a log failure the result omits, a file in the diff or its import closure, a paid or security test, another head, another PR head, a newer run, a run from another repository or fork, a stale or unfinished run, a moved attempt (also after the inventory runs), and a grant with another scope, outcome or speaker; ready still refuses red CI, and refuses an exception granted for another PR or head. The printed grant command quotes case names for the shell. These tests failed before the change. + +## [1.94.8.0] - 2026-11-03 + +**`gstack-approvals render` no longer puts the owner's exact words, autoplan run ids or plan hashes into PR bodies on public repositories (GST-1160).** On a public repo, /ship pasted the full `## Approvals` table into the PR description. That table carries the owner's verbatim quote, the run id and the plan sha256, and GitHub keeps every earlier version of a description in its edit history, which no API can delete. + +### Fixed +- **`bin/gstack-approvals render`** asks gh for the visibility of the repository gh picks by default (`GH_REPO`, its default repo, an `upstream` remote) and of the `origin` repository the PR is opened against. When every one of them is private (or internal), it prints the full table as before. When any is public, or gh can't tell, it prints only `Approved: yes|no (record r-<12 hex>)`. The record id is a hash of the row ids, so the Captain can match it to the private record without anything identifying being published. A stderr line, `APPROVALS_RENDER: redacted (repository visibility …)`, says which mode was used. +- **`--full`** prints the full table when gh can't read the visibility and you know the repository is private. It never prints the full table for a repository GitHub reports as public. `gstack-approvals list` still shows the full record. +- **/ship Step 19** (`ship/sections/capy-pr.md`) says a public repository gets only the one line, and never to pass `--full` for a PR body. + +## [1.94.7.0] - 2026-11-03 + +### Fixed + +- **/autoplan no longer stalls after a review phase when its reviewers share your machine (GST-1161).** A session installed from the skill's own folder gets an install verdict with no project, and its `GSTACK_LAUNCH` line points admission at that file. When a reviewer on the same machine ran `install.sh --for review-voice` from the project, it wrote a review-voice verdict keyed by that project. Admission read the project's file first, so the next `gstack-autoplan submit` refused with INSTALL_UNVERIFIED ("certified for=review-voice") until `install.sh --for autoplan` was run again. Both admission readers (`lib/install-verdict.ts` and `bin/gstack-install-verdict.sh`) now skip a review-voice verdict at the project path when the session's launched `GSTACK_INSTALL_VERDICT` exists, since a review-voice verdict can't admit any workflow. With no launched verdict, the review-voice refusal and its fix line are unchanged. `scripts/install.sh` and its user-scope copy don't change. + +## [1.94.6.0] - 2026-11-03 + +Two opt-in Claude Code hooks so a model actually runs gstack's skills, both off by default (GST-1088). `./setup --skill-routing` adds a UserPromptSubmit hook that names the gstack skill a prompt fits, scored against each installed skill's `triggers:` with no model call. `./setup --skill-contracts` adds a Stop hook that blocks once when a skill with a `contract:` block ends without its required outputs (a report with real values, a test after the last edit, the specialist and adversarial dispatches). `gstack-skill-route --explain` shows how a prompt scores; `gstack-skill-contract check|report|waive` checks a transcript, lists misses and acknowledges a waiver. Turn either off with `gstack-config set skill_routing off` or `gstack-config set skill_contracts off`. Docs: docs/skill-routing-and-contracts.md. + +## [1.94.5.0] - 2026-11-03 + +**Security: the lean /autoplan reviewer no longer gets `.envrc`, `.env-production` or a key pasted into an ordinary file (GST-1164, #3914).** Since v1.93.36.0 (#3897), the lean run puts the branch's draft code into a prompt that goes to an outside model provider. It left out only some secret-named untracked files: `.env` and `.env.*`, but not `.envrc` or `.env-production`. It applied no name rule to tracked files and no content check at all. So a direnv `.envrc`, a `.env-production`, a committed or edited secret file, or a key sitting in any source file could reach the provider. + +### Fixed +- **`lib/autoplan-lean.ts` `captureDraft`:** the secret-name rule now covers the whole `.env*` family, `.envrc` included, every `*credentials*` name, `_netrc`, `.pgpass`, `*.jks` and `id_*_sk` keys. Matching files are never diffed, tracked or untracked, and the draft names each one it left out. A secret-named file that git sees renamed or copied on the branch stays out under both names, even when the new name looks ordinary. A secret moved without `git add`, copied unchanged, or renamed and heavily edited still reaches the draft under its new name, guarded only by the content check. Every other file's patch goes through gstack's redact engine (`redactFindingSpans`), so a key in an ordinary file reaches the reviewer as ``. A patch the engine can't redact safely, including one over 1 MiB, is left out with a note. + +### Tests +- `test/autoplan-lean.test.ts`: an untracked `.envrc`, `.env-production` and six other secret files stay out of `draft.diff` and the prompt; a committed and edited `.envrc` stays out of the tracked diff, and so do an `.envrc` renamed to `.envrc.local` and a `.pgpass` renamed to `pg.txt`; a fake key in an untracked file and in a working-tree edit to a tracked file comes out redacted. All four fail on main at b42ad1e. + +Thanks to the /ship pre-landing review of #3901, which filed the finding. + +## [1.94.4.0] - 2026-11-03 + +- The captain skill, the CREW template, the lane brief, docs/captain.md and the Capy gstack skill now follow R65 instead of telling a lane that every fix needs a one-round, eng-only plan. A bug fix is exempt from plan review (`gstack-autoplan next --kind fix`; the brief's reason is `bug fix (R65)`), and its PR carries a repro on current main or a failing test written first. A feature gets the lean run (`--kind feature`): one reviewer reading the plan and the draft code together. The S1 and audited-problem-list exemptions are unchanged. + +## [1.94.3.0] - 2026-11-03 + +**`gstack-ship-preflight` no longer trusts a local `origin/HEAD` when origin can't say its default branch.** Preflight fell back to the local `origin/HEAD` whenever `git ls-remote --symref origin HEAD` failed or named no branch. With no local `origin/HEAD`, it went on from the default branch itself and merged the given base into it. When a PR-branch clone's `origin/HEAD` named the feature branch, it refused that branch as "on the base branch". Now a failed `ls-remote` stops with `git ls-remote origin HEAD failed: …; restore access first` before anything merges. A remote HEAD that names no branch (a detached HEAD, or a server that sends no symref) falls back to the shared default-base order in `lib/default-base.ts`, which uses the local `origin/HEAD` only when it doesn't name the checked-out branch, its upstream or HEAD's commit. The default-branch check now runs after the fetch, so a failed fetch still reports the fetch. + +### Fixed +- **`docs/eval-calls.md`, `docs/eval-models.md`**: the default `--base` now matches base resolution: origin/HEAD's branch unless it names the checked-out branch, its upstream or HEAD's commit, then the PR's base, the remote's live HEAD, `main` or `master`. + +### Tests +- `test/ship-preflight.test.ts`: with `ls-remote` failing and the fetch working, preflight stops before merging in two cases. One is the default branch with no local `origin/HEAD`, where main exited 0 and merged `release` into `main`. The other is a feature branch that the local `origin/HEAD` names, where main stopped as "on the base branch". When `ls-remote` prints only a sha, the default branch still stops as the base branch (main exited 0) and the feature branch goes on. + +## [1.94.2.0] - 2026-11-03 + +### Fixed + +- **More recursive deletes keep the security class (GST-942).** `fs.rmSync(path.join(dir, "x"), { recursive: true })`, a `)` inside a comment before `recursive: true`, `fs.rm`/`fs.promises.rm`/an imported `rm`, and a new call a formatter split across lines all slipped past the rule, because it stopped reading at the first `)` and read one line at a time. The review depth could then drop to light on real recursive-delete code. The rule now reads up to 500 characters after `rmSync(` or `rm(` across the file's changed lines, still in linear time. An edit to one line of an existing split call is still read on its own. + +## [1.94.1.0] - 2026-11-03 + +### Fixed +- **The secrets check no longer reads a file name or dict key inside a call as a credential (GST-1064).** `key = json.loads(files.get("key.json", ""))` failed pregate with `PREGATE_SECRETS` (`env.kv`): eight distinct letters in `"key.json"` reach the entropy bar, so the call's argument looked like a secret. So did `token = resp.json().get("access_token")`. The first argument of a lookup call (`get`, `getenv`, `read`, `load`, `loads`, `open`, `pop`, `fetch`) that is a lowercase file name or a snake_case key of two or three words now counts as a name. Any other call still reports a password-shaped literal (`make_password("orange_monkey_pass")`, `bcrypt.hashpw(...)`), and so do a later argument or fallback (`os.getenv("SECRET_KEY", "orange_monkey_pass")`), a real key in quotes or in a `.env` line, and a kebab passphrase (`decrypt("prod-signing-key")`). The entropy bar is unchanged. + ## [1.94.0.0] - 2026-11-03 - CI is now the one full test run. When a verified CI run already passed at the exact, clean tree, `gstack-pregate`'s tests stage and /ship's test lanes use it instead of running the suite again (GSTA-258, R65 item 1). The tests stage looks up the newest verified run at HEAD under the base branch's `run_profiles` by itself (`CI_CARRY: accepted …`; `--no-ci-carry` turns it off). A /ship lane carries only when the base branch's `.gstack/pregate.json` maps its exact command in the new `ship_lanes_from` key, and only when the run's `tree.json` covers the lane's inventory; gstack maps `bun run test` to its Free Tests run. A carried lane prints `LANE: